import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, } from "node:crypto";
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { keyFor, maskSecret } from "./SecureStore.js";
export class EncryptedFallbackSecureStore {
    filePath;
    key;
    constructor(filePath, key) {
        this.filePath = filePath;
        this.key = key;
        if (key.byteLength !== 32) {
            throw new Error("Encrypted credential fallback key must be 32 bytes.");
        }
    }
    async get(ref) {
        const file = await this.readFile();
        const record = file.records[recordKey(ref)];
        if (!record)
            return undefined;
        return decrypt(record, this.key);
    }
    async set(ref, secret) {
        const file = await this.readFile();
        file.records[recordKey(ref)] = encrypt(secret, this.key);
        await this.writeFile(file);
    }
    async delete(ref) {
        const file = await this.readFile();
        delete file.records[recordKey(ref)];
        await this.writeFile(file);
    }
    async status(ref) {
        const file = await this.readFile();
        const record = file.records[recordKey(ref)];
        if (!record)
            return { ref, state: "missing", backend: "encrypted_fallback" };
        const secret = decrypt(record, this.key);
        return {
            ref,
            state: "available",
            maskedLabel: maskSecret(secret),
            updatedAt: record.updatedAt,
            ...(record.credentialVersion
                ? { credentialVersion: record.credentialVersion }
                : {}),
            backend: "encrypted_fallback",
        };
    }
    async readFile() {
        try {
            const payload = JSON.parse(await readFile(this.filePath, "utf8"));
            if (isEncryptedFile(payload))
                return payload;
            throw new Error(`Invalid encrypted credential fallback file format: ${this.filePath}`);
        }
        catch (error) {
            if (isNodeError(error) && error.code === "ENOENT") {
                return { version: 1, records: {} };
            }
            throw error;
        }
    }
    async writeFile(file) {
        await mkdir(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
        const tmpPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
        await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}\n`, {
            mode: 0o600,
        });
        await rename(tmpPath, this.filePath);
        await assertPrivateFile(this.filePath);
    }
}
export function parseFallbackKey(value) {
    if (!value)
        return undefined;
    const trimmed = value.trim();
    const encoding = /^[a-f0-9]{64}$/i.test(trimmed) ? "hex" : "base64";
    const key = Buffer.from(trimmed, encoding);
    if (key.byteLength !== 32) {
        throw new Error("PATCHTRAIL_CREDENTIAL_FALLBACK_KEY must decode to exactly 32 bytes.");
    }
    return key;
}
function encrypt(secret, key) {
    const iv = randomBytes(12);
    const cipher = createCipheriv("aes-256-gcm", key, iv);
    const ciphertext = Buffer.concat([
        cipher.update(secret, "utf8"),
        cipher.final(),
    ]);
    const tag = cipher.getAuthTag();
    return {
        algorithm: "aes-256-gcm",
        iv: iv.toString("base64"),
        tag: tag.toString("base64"),
        ciphertext: ciphertext.toString("base64"),
        updatedAt: new Date().toISOString(),
        credentialVersion: randomUUID(),
    };
}
function decrypt(record, key) {
    const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(record.iv, "base64"));
    decipher.setAuthTag(Buffer.from(record.tag, "base64"));
    return Buffer.concat([
        decipher.update(Buffer.from(record.ciphertext, "base64")),
        decipher.final(),
    ]).toString("utf8");
}
function recordKey(ref) {
    return createHash("sha256").update(keyFor(ref)).digest("hex");
}
function isEncryptedFile(value) {
    if (typeof value !== "object" || value === null)
        return false;
    if (!("version" in value) || value.version !== 1)
        return false;
    if (!("records" in value) ||
        typeof value.records !== "object" ||
        value.records === null) {
        return false;
    }
    return true;
}
async function assertPrivateFile(filePath) {
    if (process.platform === "win32")
        return;
    const fileStat = await stat(filePath);
    const mode = fileStat.mode & 0o777;
    if (mode !== 0o600) {
        throw new Error(`Encrypted credential fallback file must be mode 0600: ${filePath}`);
    }
}
function isNodeError(error) {
    return error instanceof Error && "code" in error;
}
