import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import lockfile from "proper-lockfile";
import { getAgentDir } from "../config.js";
import { normalizePath } from "../utils/paths.js";
const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
export class FileAuthStorageBackend {
    constructor(authPath = join(getAgentDir(), "auth.json"), readPaths = [authPath]) {
        this.authPath = normalizePath(authPath);
        this.readPaths = readPaths.map((readPath) => normalizePath(readPath));
    }
    ensureParentDir() {
        const dir = dirname(this.authPath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
    }
    ensureFileExists() {
        if (!existsSync(this.authPath)) {
            writeFileSync(this.authPath, "{}", AUTH_FILE_WRITE_OPTIONS);
            chmodSync(this.authPath, 0o600);
        }
    }
    acquireLockSyncWithRetry(path) {
        const maxAttempts = 10;
        const delayMs = 20;
        let lastError;
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return lockfile.lockSync(path, { realpath: false });
            }
            catch (error) {
                const code = typeof error === "object" && error !== null && "code" in error
                    ? String(error.code)
                    : undefined;
                if (code !== "ELOCKED" || attempt === maxAttempts) {
                    throw error;
                }
                lastError = error;
                const start = Date.now();
                while (Date.now() - start < delayMs) {
                    // Sleep synchronously to avoid changing callers to async.
                }
            }
        }
        throw lastError ?? new Error("Failed to acquire auth storage lock");
    }
    readMergedAuth() {
        let merged = {};
        let found = false;
        for (let i = this.readPaths.length - 1; i >= 0; i--) {
            const readPath = this.readPaths[i];
            if (!existsSync(readPath))
                continue;
            const parsed = JSON.parse(readFileSync(readPath, "utf-8"));
            merged = { ...merged, ...parsed };
            found = true;
        }
        return found ? JSON.stringify(merged, null, 2) : undefined;
    }
    /**
     * Atomically replace `auth.json` with `content`: write a sibling temp file
     * (same directory, so `rename` is a same-filesystem atomic swap), fix its
     * permissions, then `rename` it over the target. Lock-free readers therefore
     * never observe a half-written file. The temp file is best-effort cleaned up
     * if the rename fails.
     */
    writeAtomic(content) {
        const dir = dirname(this.authPath);
        const tempPath = join(dir, `.${`auth.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`}.tmp`);
        try {
            writeFileSync(tempPath, content, AUTH_FILE_WRITE_OPTIONS);
            chmodSync(tempPath, 0o600);
            renameSync(tempPath, this.authPath);
        }
        catch (error) {
            try {
                if (existsSync(tempPath))
                    rmSync(tempPath, { force: true });
            }
            catch {
                // Best-effort cleanup; ignore.
            }
            throw error;
        }
    }
    read() {
        return this.readMergedAuth();
    }
    withLock(fn) {
        this.ensureParentDir();
        let release;
        try {
            if (existsSync(this.authPath)) {
                release = this.acquireLockSyncWithRetry(this.authPath);
            }
            const current = this.readMergedAuth();
            const { result, next } = fn(current);
            if (next !== undefined) {
                if (!existsSync(this.authPath)) {
                    this.ensureFileExists();
                }
                if (!release) {
                    release = this.acquireLockSyncWithRetry(this.authPath);
                }
                this.writeAtomic(next);
            }
            return result;
        }
        finally {
            if (release) {
                release();
            }
        }
    }
    async withLockAsync(fn) {
        this.ensureParentDir();
        let release;
        let lockCompromised = false;
        let lockCompromisedError;
        const throwIfCompromised = () => {
            if (lockCompromised) {
                throw lockCompromisedError ?? new Error("Auth storage lock was compromised");
            }
        };
        try {
            if (!existsSync(this.authPath)) {
                this.ensureFileExists();
            }
            release = await lockfile.lock(this.authPath, {
                retries: {
                    retries: 10,
                    factor: 2,
                    minTimeout: 100,
                    maxTimeout: 10000,
                    randomize: true,
                },
                stale: 30000,
                onCompromised: (err) => {
                    lockCompromised = true;
                    lockCompromisedError = err;
                },
            });
            throwIfCompromised();
            const current = this.readMergedAuth();
            const { result, next } = await fn(current);
            throwIfCompromised();
            if (next !== undefined) {
                this.writeAtomic(next);
            }
            throwIfCompromised();
            return result;
        }
        finally {
            if (release) {
                try {
                    await release();
                }
                catch {
                    // Ignore unlock errors when lock is compromised.
                }
            }
        }
    }
}
export class InMemoryAuthStorageBackend {
    read() {
        return this.value;
    }
    withLock(fn) {
        const { result, next } = fn(this.value);
        if (next !== undefined) {
            this.value = next;
        }
        return result;
    }
    async withLockAsync(fn) {
        const { result, next } = await fn(this.value);
        if (next !== undefined) {
            this.value = next;
        }
        return result;
    }
}
//# sourceMappingURL=auth-storage-backends.js.map