const { SlashCommandBuilder } = require('discord.js');
const axios = require('axios');
const os = require('node:os');
const fs = require('node:fs/promises');
const path = require('node:path');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');

const { getAllLinks } = require('../../src/core/database.js');
const {
    defaultBaseUrl,
    defaultServerId,
    listDirectory,
    downloadFile,
    uploadSingleFile,
    deleteFiles,
} = require('../../src/pterodactyl/pterodactylFiles.js');

const execFileAsync = promisify(execFile);

const ROLE_RYGAINLAND = '1444684935632912394';

const keepListFilePath = path.resolve(__dirname, 'server-clean-keep.json');

const normUid32 = (val) => {
    if (!val) return '';
    const s = String(val).replace(/-/g, '').trim().toLowerCase();
    if (s.length !== 32) return '';
    if (!/^[0-9a-f]{32}$/.test(s)) return '';
    return s;
};

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const parseCsv = (val) =>
    String(val || '')
        .split(',')
        .map((x) => x.trim())
        .filter(Boolean);

async function loadKeepList() {
    const keepDiscordIds = new Set(parseCsv(process.env.SERVER_CLEAN_KEEP_DISCORD_IDS));
    const keepPlayerIds = new Set(parseCsv(process.env.SERVER_CLEAN_KEEP_PLAYER_IDS).map(normUid32).filter(Boolean));

    try {
        const raw = await fs.readFile(keepListFilePath, 'utf8');
        const parsed = JSON.parse(raw);

        if (Array.isArray(parsed.discordIds)) {
            for (const id of parsed.discordIds) {
                const clean = String(id || '').trim();
                if (clean) keepDiscordIds.add(clean);
            }
        }

        if (Array.isArray(parsed.playerIds)) {
            for (const uid of parsed.playerIds) {
                const clean = normUid32(uid);
                if (clean) keepPlayerIds.add(clean);
            }
        }
    } catch (error) {
        if (error && error.code !== 'ENOENT') {
            throw new Error(`Impossible de lire ${path.basename(keepListFilePath)}: ${error.message || String(error)}`);
        }
    }

    return {
        keepDiscordIds,
        keepPlayerIds,
    };
}

async function waitForServerState({ headers, baseUrl, serverId, wanted, timeoutMs }) {
    const started = Date.now();
    while (Date.now() - started < timeoutMs) {
        const status = await axios.get(`${baseUrl}/api/client/servers/${serverId}/resources`, { headers });
        const state = status?.data?.attributes?.current_state;
        if (state === wanted) return true;
        await sleep(5000);
    }
    return false;
}

async function detectWorldId({ headers, baseUrl, serverId, saveRoot }) {
    const envWorldId = (process.env.PALWORLD_WORLD_ID || '').trim();
    if (envWorldId) return envWorldId;

    const items = await listDirectory({ headers, baseUrl, serverId, directory: saveRoot });
    const dirs = items.filter((x) => x && x.is_file === false && typeof x.name === 'string');

    if (dirs.length === 1) return dirs[0].name;

    const names = dirs.map((d) => d.name).slice(0, 10).join(', ');
    throw new Error(
        `Impossible de déterminer automatiquement le World ID. Définis PALWORLD_WORLD_ID dans .env. Dossiers trouvés dans ${saveRoot}: ${names || '(aucun)'}`
    );
}

async function runPlayersDeleter({ levelSavPath, uids }) {
    const execPath = (process.env.PLAYERS_DELETER_PATH || '').trim() || path.resolve(__dirname, '../../players-deleter');

    // The shipped binary is Linux. If you run the bot on Windows, provide an alternate command.
    if (process.platform === 'win32') {
        throw new Error(
            "players-deleter est un exécutable Linux. Lance le bot sous Linux/WSL, ou définis PLAYERS_DELETER_PATH vers une commande compatible (ex: script Python)."
        );
    }

    const args = [
        '--level-sav-path',
        levelSavPath,
        '--no-backup',
        '--keep-player-files',
    ];

    for (const uid of uids) {
        args.push('--uid', uid);
    }

    if ((process.env.PLAYERS_DELETER_ALLOW_ZLIB_FALLBACK || '').toLowerCase() === 'true') {
        args.push('--allow-zlib-fallback');
    }

    // Best effort: ensure the binary has execute permission on Linux containers.
    await fs.chmod(execPath, 0o755).catch(() => {});

    let stdout = '';
    let stderr = '';
    try {
        const result = await execFileAsync(execPath, args, { timeout: 10 * 60 * 1000 });
        stdout = result.stdout;
        stderr = result.stderr;
    } catch (error) {
        if (error && error.code === 'EACCES') {
            throw new Error(
                `Impossible d'exécuter players-deleter (${execPath}): permission refusée (EACCES). ` +
                    `Vérifie que le fichier est exécutable (chmod +x) et que le filesystem n'est pas monté en noexec. ` +
                    `Tu peux aussi définir PLAYERS_DELETER_PATH vers une commande exécutable.`
            );
        }
        throw error;
    }

    const out = String(stdout || '').trim();
    if (out) {
        try {
            return JSON.parse(out);
        } catch {
            // If not JSON, return raw.
            return { ok: true, stdout: out, stderr: String(stderr || '').trim() };
        }
    }

    if (stderr) {
        return { ok: true, stdout: '', stderr: String(stderr).trim() };
    }

    return { ok: true };
}

module.exports = {
    data: new SlashCommandBuilder()
        .setName('server-clean')
        .setDescription('Supprime les joueurs inactifs (3 mois) ou absents du Discord, puis redéploie la sauvegarde'),

    async execute(interaction, headers) {
        if (!interaction.member.roles.cache.has(ROLE_RYGAINLAND)) {
            await interaction.reply({
                content: '❌ Il faut avoir le rôle Rygainland pour pouvoir utiliser cette commande.',
                flags: 64,
            });
            return;
        }

        await interaction.deferReply();

        const baseUrl = defaultBaseUrl();
        const serverId = defaultServerId();
        const apiHeaders = headers;

        try {
            await interaction.editReply('🔎 Recherche des joueurs inactifs...');

            const links = await getAllLinks();

            const threeMonthsAgo = new Date();
            threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);

            const targetLinks = [];
            const reasonsByDiscordId = new Map();

            for (const link of links) {
                const discordId = String(link.discord_id);
                const member = await interaction.guild.members.fetch(discordId).catch(() => null);

                const reasons = [];
                if (!member) {
                    reasons.push('absent_discord');
                }

                if (link.lastConnection) {
                    const lastConn = new Date(link.lastConnection);
                    if (lastConn < threeMonthsAgo) {
                        reasons.push('inactif_3_mois');
                    }
                }

                if (reasons.length > 0) {
                    targetLinks.push(link);
                    reasonsByDiscordId.set(discordId, reasons);
                }
            }

            // De-dupe by discord_id.
            const byDiscord = new Map();
            for (const l of targetLinks) byDiscord.set(String(l.discord_id), l);
            const deduped = [...byDiscord.values()];

            const keepList = await loadKeepList();
            const filteredLinks = [];
            let keepListExcludedCount = 0;

            for (const link of deduped) {
                const discordId = String(link.discord_id || '').trim();
                const uid = normUid32(link.player_id);

                if (keepList.keepDiscordIds.has(discordId) || (uid && keepList.keepPlayerIds.has(uid))) {
                    keepListExcludedCount += 1;
                    continue;
                }

                filteredLinks.push(link);
            }

            const uidsToDelete = [];
            const missingPlayerId = [];

            for (const link of filteredLinks) {
                const uid = normUid32(link.player_id);
                if (!uid) {
                    missingPlayerId.push(link);
                    continue;
                }
                uidsToDelete.push(uid);
            }

            if (uidsToDelete.length === 0) {
                const extra = missingPlayerId.length
                    ? `\n⚠️ ${missingPlayerId.length} membre(s) ciblé(s) sans Player ID en DB (impossible à supprimer automatiquement).`
                    : '';
                const keepText = keepListExcludedCount
                    ? `\n🛡️ Exclus via liste blanche: ${keepListExcludedCount}`
                    : '';
                await interaction.editReply(`✅ Aucun joueur supprimable trouvé.${extra}${keepText}`);
                return;
            }

            await interaction.editReply(
                `🧹 Joueurs ciblés: **${uidsToDelete.length}** (inactifs/absents).\n` +
                    `💾 Sauvegarde serveur...`
            );

            // Check current state.
            const statusResponse = await axios.get(`${baseUrl}/api/client/servers/${serverId}/resources`, { headers: apiHeaders });
            const currentState = statusResponse.data.attributes.current_state;

            if (currentState === 'running') {
                await axios.post(
                    `${baseUrl}/api/client/servers/${serverId}/command`,
                    { command: 'save' },
                    { headers: apiHeaders }
                );
                await axios
                    .post(
                        `${baseUrl}/api/client/servers/${serverId}/command`,
                        { command: "broadcast 'Nettoyage des joueurs inactifs (arrêt serveur) - ~60s'" },
                        { headers: apiHeaders }
                    )
                    .catch(() => {});

                await sleep(3000);

                await interaction.editReply('⏹️ Arrêt du serveur...');
                await axios.post(
                    `${baseUrl}/api/client/servers/${serverId}/power`,
                    { signal: 'stop' },
                    { headers: apiHeaders }
                );

                const stopped = await waitForServerState({
                    headers: apiHeaders,
                    baseUrl,
                    serverId,
                    wanted: 'offline',
                    timeoutMs: 3 * 60 * 1000,
                });
                if (!stopped) {
                    await interaction.editReply('⚠️ Le serveur ne passe pas offline, tentative de continuer quand même...');
                }
            } else {
                await interaction.editReply('ℹ️ Serveur déjà arrêté. Passage à la récupération de la sauvegarde...');
            }

            const saveRoot = (process.env.PALWORLD_SAVE_ROOT || '/Pal/Saved/SaveGames/0').trim();
            const worldId = await detectWorldId({ headers: apiHeaders, baseUrl, serverId, saveRoot });
            const worldDir = `${saveRoot}/${worldId}`;
            const levelSavRemote = `${worldDir}/Level.sav`;

            await interaction.editReply(`📥 Téléchargement de Level.sav (World: ${worldId})...`);
            const levelSavBuf = await downloadFile({ headers: apiHeaders, baseUrl, serverId, file: levelSavRemote });

            const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'server-clean-'));
            const localLevelSav = path.join(tmpDir, 'Level.sav');
            await fs.writeFile(localLevelSav, levelSavBuf);

            await interaction.editReply('🧬 Suppression des joueurs inactifs dans la sauvegarde...');
            const deleterReport = await runPlayersDeleter({ levelSavPath: localLevelSav, uids: uidsToDelete });

            const newLevelSavBuf = await fs.readFile(localLevelSav);

            await interaction.editReply('📤 Upload de Level.sav modifié...');
            await uploadSingleFile({
                headers: apiHeaders,
                baseUrl,
                serverId,
                directory: worldDir,
                filename: 'Level.sav',
                content: newLevelSavBuf,
            });

            await interaction.editReply('🗑️ Suppression des fichiers Players/<uid>.sav...');
            const playersDir = `${worldDir}/Players`;

            const filesToDelete = [];
            for (const uid of uidsToDelete) {
                filesToDelete.push(`${uid}.sav`);
                filesToDelete.push(`${uid}_dps.sav`);
            }

            // Chunk deletes to keep requests reasonable.
            const CHUNK = 100;
            for (let i = 0; i < filesToDelete.length; i += CHUNK) {
                const slice = filesToDelete.slice(i, i + CHUNK);
                await deleteFiles({ headers: apiHeaders, baseUrl, serverId, root: playersDir, files: slice }).catch(() => {});
            }

            await interaction.editReply('🚀 Démarrage du serveur...');
            await axios.post(
                `${baseUrl}/api/client/servers/${serverId}/power`,
                { signal: 'start' },
                { headers: apiHeaders }
            );

            const running = await waitForServerState({
                headers: apiHeaders,
                baseUrl,
                serverId,
                wanted: 'running',
                timeoutMs: 4 * 60 * 1000,
            });

            const missingText = missingPlayerId.length
                ? `\n⚠️ Non supprimés (pas de Player ID en DB): ${missingPlayerId.length}`
                : '';

            const keepText = keepListExcludedCount
                ? `\n🛡️ Exclus via liste blanche: ${keepListExcludedCount}`
                : '';

            const okText = running ? '✅ Nettoyage terminé et serveur redémarré.' : '⚠️ Nettoyage terminé, mais le serveur n\'a pas confirmé son état running.';

            // Keep output short; deleterReport can be long.
            const reportSummary = deleterReport && typeof deleterReport === 'object'
                ? JSON.stringify(deleterReport).slice(0, 900)
                : '';

            await interaction.editReply(
                `${okText}\n` +
                    `👥 Joueurs supprimés (ciblés): ${uidsToDelete.length}${missingText}${keepText}\n` +
                    (reportSummary ? `📄 players-deleter: ${reportSummary}` : '')
            );

            await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
        } catch (error) {
            console.error('Erreur /server-clean:', error);
            await interaction.editReply(`❌ Erreur pendant /server-clean: ${error.message || String(error)}`);
        }
    },
};
