import * as fs from "node:fs";
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import archiver from "archiver";
import config from "../../../../config/index.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
import { createTempDir } from "../../../../misc/create-temp.js";
export const meta = {
    tags: [
        "admin"
    ],
    requireCredential: true,
    requireAdmin: true,
    res: {
        type: "object",
        optional: false,
        nullable: false,
        properties: {
            path: {
                type: "string",
                optional: false,
                nullable: false
            },
            fileName: {
                type: "string",
                optional: false,
                nullable: false
            },
            size: {
                type: "number",
                optional: false,
                nullable: false
            },
            includedMedia: {
                type: "boolean",
                optional: false,
                nullable: false
            }
        }
    },
    errors: {
        backupFailed: {
            message: "Failed to create backup.",
            code: "BACKUP_FAILED",
            id: "7498ab9f-4e1d-40d0-96d8-d8d1d82bd621"
        }
    }
};
export const paramDef = {
    type: "object",
    properties: {},
    required: []
};
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../../..");
function timestamp() {
    const now = new Date();
    const pad = (value)=>value.toString().padStart(2, "0");
    return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
}
function run(command, args, env) {
    return new Promise((resolvePromise, reject)=>{
        const child = spawn(command, args, {
            stdio: [
                "ignore",
                "ignore",
                "pipe"
            ],
            env
        });
        let stderr = "";
        child.stderr.on("data", (chunk)=>{
            stderr += chunk.toString();
        });
        child.on("error", reject);
        child.on("exit", (code)=>{
            if (code === 0) {
                resolvePromise();
            } else {
                reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
            }
        });
    });
}
function archiveDirectory(sourceDir, outFile) {
    return new Promise((resolvePromise, reject)=>{
        const output = fs.createWriteStream(outFile);
        const archive = archiver("tar", {
            gzip: true,
            gzipOptions: {
                level: 6
            }
        });
        output.on("close", ()=>resolvePromise());
        archive.on("error", reject);
        archive.pipe(output);
        archive.directory(sourceDir, false);
        archive.finalize();
    });
}
async function pathExists(path) {
    return stat(path).then(()=>true).catch(()=>false);
}
export default define(meta, paramDef, async (_ps, me)=>{
    const [workDir, cleanup] = await createTempDir();
    const fileName = `iceshrimp-full-${timestamp()}.tar.gz`;
    const outDir = resolve(process.env.ICESHRIMP_BACKUP_DIR ?? `${rootDir}/backups`);
    const outFile = resolve(outDir, fileName);
    const dbDumpPath = resolve(workDir, "database.dump");
    const mediaDir = resolve(config.mediaDir);
    let includedMedia = false;
    try {
        await mkdir(outDir, {
            recursive: true
        });
        const env = {
            ...process.env,
            PGHOST: config.db.host,
            PGPORT: String(config.db.port),
            PGDATABASE: config.db.db,
            PGUSER: config.db.user,
            PGPASSWORD: config.db.pass
        };
        await run("pg_dump", [
            "--format=custom",
            "--blobs",
            "--no-owner",
            "--file",
            dbDumpPath,
            config.db.db
        ], env);
        const configDir = resolve(workDir, "config");
        await mkdir(configDir, {
            recursive: true
        });
        const configFiles = [
            process.env.ICESHRIMP_CONFIG ? resolve(process.env.ICESHRIMP_CONFIG) : resolve(rootDir, ".config/default.yml"),
            ...process.env.ICESHRIMP_SECRETS ? [
                resolve(process.env.ICESHRIMP_SECRETS)
            ] : []
        ];
        for (const configFile of configFiles){
            if (await pathExists(configFile)) {
                fs.copyFileSync(configFile, resolve(configDir, configFile.split("/").pop()));
            }
        }
        if (await pathExists(mediaDir) && !outDir.startsWith(mediaDir + "/") && outDir !== mediaDir) {
            fs.cpSync(mediaDir, resolve(workDir, "files"), {
                recursive: true,
                dereference: false,
                errorOnExist: false
            });
            includedMedia = true;
        }
        await writeFile(resolve(workDir, "manifest.json"), `${JSON.stringify({
            type: "iceshrimp-full-backup",
            version: config.version,
            createdAt: new Date().toISOString(),
            host: config.host,
            database: config.db.db,
            included: {
                database: true,
                config: true,
                media: includedMedia
            },
            restore: "yarn full:restore <this archive>"
        }, null, 2)}\n`, "utf8");
        await archiveDirectory(workDir, outFile);
        const outStat = await stat(outFile);
        await insertModerationLog(me, "createBackup", {
            path: outFile,
            size: outStat.size,
            includedMedia
        });
        return {
            path: outFile,
            fileName,
            size: outStat.size,
            includedMedia
        };
    } catch (e) {
        await rm(outFile, {
            force: true
        }).catch(()=>{});
        throw new ApiError(meta.errors.backupFailed, {
            message: e instanceof Error ? e.message : String(e)
        });
    } finally{
        cleanup();
    }
});
