import { randomUUID } from "node:crypto";
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import {} from "./plugin-types.js";
const PLUGIN_JOB_LIMIT = 200;
const PLUGIN_JOB_WRITE_LOCKS = new Map();
const WINDOWS_RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
export class PluginStore {
    root;
    installedRoot;
    logRoot;
    dataRoot;
    registryPath;
    lockPath;
    jobsPath;
    constructor(home) {
        this.root = path.join(home, "plugins");
        this.installedRoot = path.join(this.root, "installed");
        this.logRoot = path.join(this.root, "logs");
        this.dataRoot = path.join(this.root, "data");
        this.registryPath = path.join(this.root, "plugins.json");
        this.lockPath = path.join(this.root, "plugins.lock.json");
        this.jobsPath = path.join(this.root, "jobs.json");
    }
    async ensure() {
        await mkdir(this.installedRoot, { recursive: true });
        await mkdir(this.logRoot, { recursive: true });
        await mkdir(this.dataRoot, { recursive: true });
    }
    async list() {
        const payload = await this.read();
        return payload.plugins.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
    }
    async get(id) {
        const payload = await this.read();
        return payload.plugins.find((plugin) => plugin.id === id);
    }
    async save(plugin) {
        const payload = await this.read();
        const index = payload.plugins.findIndex((item) => item.id === plugin.id);
        if (index >= 0) {
            payload.plugins[index] = plugin;
        }
        else {
            payload.plugins.push(plugin);
        }
        await this.write(payload);
    }
    async remove(id) {
        const payload = await this.read();
        const plugin = payload.plugins.find((item) => item.id === id);
        const next = payload.plugins.filter((item) => item.id !== id);
        if (next.length === payload.plugins.length) {
            return false;
        }
        await this.write({ ...payload, plugins: next });
        if (plugin?.installPath) {
            await rm(plugin.installPath, { recursive: true, force: true });
        }
        await this.removeLock(id);
        return true;
    }
    async read() {
        await this.ensure();
        try {
            const raw = await readFile(this.registryPath, "utf8");
            const parsed = JSON.parse(raw);
            if (parsed.version !== 1 || !Array.isArray(parsed.plugins)) {
                return { version: 1, plugins: [] };
            }
            return { version: 1, plugins: parsed.plugins };
        }
        catch (error) {
            if (error.code === "ENOENT") {
                return { version: 1, plugins: [] };
            }
            throw error;
        }
    }
    async write(payload) {
        await this.ensure();
        await writeJsonFileAtomic(this.registryPath, payload);
    }
    async readLocks() {
        await this.ensure();
        try {
            const raw = await readFile(this.lockPath, "utf8");
            const parsed = JSON.parse(raw);
            if (parsed.version !== 1 || !Array.isArray(parsed.plugins)) {
                return { version: 1, plugins: [] };
            }
            return { version: 1, plugins: parsed.plugins };
        }
        catch (error) {
            if (error.code === "ENOENT") {
                return { version: 1, plugins: [] };
            }
            throw error;
        }
    }
    async saveLock(record) {
        const payload = await this.readLocks();
        const index = payload.plugins.findIndex((item) => item.id === record.id);
        if (index >= 0) {
            payload.plugins[index] = record;
        }
        else {
            payload.plugins.push(record);
        }
        await this.writeLocks(payload);
    }
    async removeLock(id) {
        const payload = await this.readLocks();
        const next = payload.plugins.filter((item) => item.id !== id);
        if (next.length !== payload.plugins.length) {
            await this.writeLocks({ ...payload, plugins: next });
        }
    }
    async writeLocks(payload) {
        await this.ensure();
        await writeJsonFileAtomic(this.lockPath, payload);
    }
    async readJobs() {
        await this.ensure();
        try {
            const raw = await readFile(this.jobsPath, "utf8");
            const parsed = JSON.parse(raw);
            if (parsed.version !== 1 || !Array.isArray(parsed.jobs)) {
                return { version: 1, jobs: [] };
            }
            return { version: 1, jobs: parsed.jobs };
        }
        catch (error) {
            if (error.code === "ENOENT") {
                return { version: 1, jobs: [] };
            }
            throw error;
        }
    }
    async writeJobs(payload) {
        await this.withJobWriteLock(async () => {
            await this.ensure();
            const jobs = payload.jobs.slice(0, PLUGIN_JOB_LIMIT).map(cloneJob);
            await writeJsonFileAtomic(this.jobsPath, { version: 1, jobs });
        });
    }
    async listJobs(pluginId) {
        const payload = await this.readJobs();
        const jobs = pluginId ? payload.jobs.filter((job) => job.pluginId === pluginId) : payload.jobs;
        return jobs.map(cloneJob);
    }
    async getJob(pluginId, jobId) {
        const payload = await this.readJobs();
        const job = payload.jobs.find((item) => item.pluginId === pluginId && item.id === jobId);
        return job ? cloneJob(job) : undefined;
    }
    async upsertJob(job) {
        return this.withJobWriteLock(async () => {
            const payload = await this.readJobs();
            const next = cloneJob(job);
            const index = payload.jobs.findIndex((item) => item.pluginId === next.pluginId && item.id === next.id);
            if (index >= 0) {
                payload.jobs[index] = next;
            }
            else {
                payload.jobs.unshift(next);
            }
            payload.jobs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
            await writeJsonFileAtomic(this.jobsPath, { version: 1, jobs: payload.jobs.slice(0, PLUGIN_JOB_LIMIT) });
            return cloneJob(next);
        });
    }
    async withJobWriteLock(fn) {
        const key = this.jobsPath;
        const previous = PLUGIN_JOB_WRITE_LOCKS.get(key) ?? Promise.resolve();
        let release;
        const current = new Promise((resolve) => { release = resolve; });
        const queued = previous.catch(() => undefined).then(() => current);
        PLUGIN_JOB_WRITE_LOCKS.set(key, queued);
        try {
            await previous.catch(() => undefined);
            return await fn();
        }
        finally {
            release();
            if (PLUGIN_JOB_WRITE_LOCKS.get(key) === queued) {
                PLUGIN_JOB_WRITE_LOCKS.delete(key);
            }
        }
    }
    installVersionPath(id, version) {
        return path.join(this.installedRoot, id, version);
    }
    logPath(id) {
        return path.join(this.logRoot, `${id}.log`);
    }
    dataPath(id) {
        return path.join(this.dataRoot, id);
    }
    async installedVersions(id) {
        try {
            const entries = await readdir(path.join(this.installedRoot, id), { withFileTypes: true });
            return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort(compareVersionsDesc);
        }
        catch (error) {
            if (error.code === "ENOENT")
                return [];
            throw error;
        }
    }
}
async function writeJsonFileAtomic(filePath, payload) {
    const dir = path.dirname(filePath);
    await mkdir(dir, { recursive: true });
    const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
    await writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
    try {
        await renameWithRetry(tmp, filePath);
    }
    catch (error) {
        await rm(tmp, { force: true }).catch(() => undefined);
        throw error;
    }
}
async function renameWithRetry(source, target) {
    const attempts = os.platform() === "win32" ? 12 : 3;
    for (let attempt = 0; attempt < attempts; attempt += 1) {
        try {
            await rename(source, target);
            return;
        }
        catch (error) {
            const code = error.code ?? "";
            if (!WINDOWS_RENAME_RETRY_CODES.has(code) || attempt >= attempts - 1) {
                throw error;
            }
            await sleep(Math.min(500, 25 * 2 ** attempt));
        }
    }
}
function cloneJob(job) {
    return JSON.parse(JSON.stringify(job));
}
function compareVersionsDesc(left, right) {
    return right.localeCompare(left, undefined, { numeric: true, sensitivity: "base" });
}
export function toPublicPluginRecord(plugin) {
    const settings = {};
    const settingsSummary = {};
    for (const setting of plugin.settingsSchema) {
        const value = plugin.settings[setting.key];
        if (setting.type === "secret") {
            settings[setting.key] = "";
            settingsSummary[setting.key] = value ? "configured" : "empty";
        }
        else {
            settings[setting.key] = value;
            settingsSummary[setting.key] = value ?? "empty";
        }
    }
    return {
        ...plugin,
        settings,
        settingsSummary,
    };
}
export function defaultPluginHome() {
    return path.join(os.homedir(), ".nordrelay");
}
