const { app, BrowserWindow, dialog, ipcMain, shell, screen, safeStorage, powerMonitor } = require('electron');
const path = require('path');
const fs = require('fs');

// --- Secure session persistence ---
const SESSION_FILE = path.join(app.getPath('userData'), 'secure-session.dat');
const https = require('https');
const http = require('http');
const { checkForUpdates } = require('./updater.cjs');

function loadEnvVar(varName) {
    const envFiles = [
        path.join(__dirname, '../../.env.local'),
        path.join(__dirname, '../../.env'),
    ];
    for (const envFile of envFiles) {
        try {
            const content = fs.readFileSync(envFile, 'utf8');
            const match = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
            if (match) return match[1].trim().replace(/^['"]|['"]$/g, '');
        } catch {}
    }
    return null;
}

// --- Settings persistence ---
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
const DEFAULT_SETTINGS = {
    windowX: undefined,
    windowY: undefined,
    windowWidth: 1200,
    windowHeight: 800,
    isMaximized: false,
    theme: 'theme-dark',
};

let mainWindow = null;

// ───────────────────────────────────────────────────────────────
// Voice recording — per-participant audio capture
// ───────────────────────────────────────────────────────────────
//
// Each active session owns:
//   - A root directory (user-configurable, defaults to
//     `userData/recordings`).
//   - A manifest file (session.json) atomically rewritten whenever
//     participants join/leave or the session ends.
//   - One append-only WebM file per participant.
//
// `recordingSessions` keyed by sessionId → { rootDir, dir, streams }
// tracks per-session state. `streams` is a map keyed by
// participantId → fs.WriteStream so append/close can find the
// right handle. Crash safety: every 20 chunks we call stream.sync
// via fsync for the open fd, so at most ~10 seconds of audio is
// unflushed at any moment.

const DEFAULT_RECORDING_ROOT = path.join(app.getPath('userData'), 'recordings');
const RECORDING_FSYNC_INTERVAL = 20; // chunks between fsyncs

const recordingSessions = new Map();

function sanitizeFilenameSegment(s) {
    return String(s || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
}

async function atomicWriteJson(filePath, obj) {
    // Write to a sibling tempfile first, then rename — on a crash
    // we either see the previous valid JSON or the new valid JSON,
    // never a half-written file.
    const tmp = `${filePath}.tmp-${Date.now()}`;
    await fs.promises.writeFile(tmp, JSON.stringify(obj, null, 2), 'utf8');
    await fs.promises.rename(tmp, filePath);
}

async function readManifest(sessionDir) {
    try {
        const raw = await fs.promises.readFile(path.join(sessionDir, 'session.json'), 'utf8');
        return JSON.parse(raw);
    } catch {
        return null;
    }
}

async function writeManifest(sessionDir, manifest) {
    await atomicWriteJson(path.join(sessionDir, 'session.json'), manifest);
}

async function updateManifest(sessionDir, updater) {
    const current = await readManifest(sessionDir);
    if (!current) return;
    const next = updater(current) ?? current;
    await writeManifest(sessionDir, next);
}

async function isWritableDir(dir) {
    try {
        await fs.promises.mkdir(dir, { recursive: true });
        const probe = path.join(dir, `.brycord-write-test-${Date.now()}`);
        await fs.promises.writeFile(probe, 'ok');
        await fs.promises.unlink(probe);
        return true;
    } catch {
        return false;
    }
}

function closeSessionStreamsSync(sessionId) {
    const session = recordingSessions.get(sessionId);
    if (!session) return;
    for (const [, entry] of session.streams) {
        try { entry.stream.end(); } catch {}
    }
    session.streams.clear();
}

function loadSettings() {
    try {
        const data = fs.readFileSync(SETTINGS_FILE, 'utf8');
        return { ...DEFAULT_SETTINGS, ...JSON.parse(data) };
    } catch {
        return { ...DEFAULT_SETTINGS };
    }
}

function saveSettings(settings) {
    try {
        fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
    } catch (err) {
        console.error('Failed to save settings:', err.message);
    }
}

function isPositionOnScreen(x, y, w, h) {
    const displays = screen.getAllDisplays();
    const MIN_OVERLAP = 100;
    return displays.some(display => {
        const { x: dx, y: dy, width: dw, height: dh } = display.bounds;
        const overlapX = Math.max(0, Math.min(x + w, dx + dw) - Math.max(x, dx));
        const overlapY = Math.max(0, Math.min(y + h, dy + dh) - Math.max(y, dy));
        return overlapX >= MIN_OVERLAP && overlapY >= MIN_OVERLAP;
    });
}

function createWindow() {
    const settings = loadSettings();

    const windowOptions = {
        width: settings.windowWidth,
        height: settings.windowHeight,
        icon: path.join(__dirname, 'icon.png'),
        frame: false,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            preload: path.join(__dirname, 'preload.cjs'),
            sandbox: false
        }
    };

    // Only restore position if it's valid on a connected display
    if (settings.windowX !== undefined && settings.windowY !== undefined &&
        isPositionOnScreen(settings.windowX, settings.windowY, settings.windowWidth, settings.windowHeight)) {
        windowOptions.x = settings.windowX;
        windowOptions.y = settings.windowY;
    }

    mainWindow = new BrowserWindow(windowOptions);

    // Fix LiveKit WebSocket/CORS for dev mode — rewrite Origin header and inject CORS response headers
    const livekitWssUrl = loadEnvVar('VITE_LIVEKIT_URL');
    if (livekitWssUrl) {
        const livekitHttpsOrigin = livekitWssUrl.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://');
        const livekitHost = new URL(livekitHttpsOrigin).hostname;
        const filter = { urls: [`*://${livekitHost}/*`] };

        mainWindow.webContents.session.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
            details.requestHeaders['Origin'] = livekitHttpsOrigin;
            callback({ requestHeaders: details.requestHeaders });
        });

        mainWindow.webContents.session.webRequest.onHeadersReceived(filter, (details, callback) => {
            const headers = details.responseHeaders || {};
            headers['Access-Control-Allow-Origin'] = ['*'];
            headers['Access-Control-Allow-Headers'] = ['*'];
            headers['Access-Control-Allow-Methods'] = ['GET, POST, OPTIONS'];
            callback({ responseHeaders: headers });
        });
    }

    if (settings.isMaximized) {
        mainWindow.maximize();
    }

    // Save window state on close
    mainWindow.on('close', () => {
        // Flush localStorage/sessionStorage to disk before renderer is destroyed
        mainWindow.webContents.session.flushStorageData();

        // Close any still-open voice recording streams so buffered
        // chunks are flushed. Manifests will still show endedAt:
        // null (the recovery modal will catch them on next launch).
        for (const [sessionId] of recordingSessions) {
            closeSessionStreamsSync(sessionId);
        }

        const current = loadSettings(); // re-read to preserve theme changes
        if (!mainWindow.isMaximized()) {
            const bounds = mainWindow.getBounds();
            current.windowX = bounds.x;
            current.windowY = bounds.y;
            current.windowWidth = bounds.width;
            current.windowHeight = bounds.height;
        }
        current.isMaximized = mainWindow.isMaximized();
        saveSettings(current);
    });

    const isDev = process.env.npm_lifecycle_event === 'electron:dev';

    if (isDev) {
        mainWindow.loadURL(process.env.VITE_DEV_URL || 'http://localhost:5173');
        mainWindow.webContents.openDevTools();
    } else {
        // Production: Load the built file
        mainWindow.loadFile(path.join(__dirname, 'dist-react', 'index.html'));
    }
}

function createSplashWindow() {
    const splash = new BrowserWindow({
        width: 300,
        height: 350,
        frame: false,
        resizable: false,
        alwaysOnTop: true,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true
        }
    });
    splash.loadFile(path.join(__dirname, 'splash.html'));
    return splash;
}

app.whenReady().then(async () => {
    const isDev = !app.isPackaged;

    if (isDev) {
        createWindow();
    } else {
        const splash = createSplashWindow();
        const noUpdate = await checkForUpdates(splash);
        if (noUpdate === false) {
            if (!splash.isDestroyed()) splash.close();
            createWindow();
        }
        // If update downloaded, quitAndInstall handles restart
    }

    ipcMain.on('window-minimize', () => {
        const win = BrowserWindow.getFocusedWindow();
        if (win) win.minimize();
    });
    ipcMain.on('window-maximize', () => {
        const win = BrowserWindow.getFocusedWindow();
        if (win) {
            if (win.isMaximized()) win.unmaximize();
            else win.maximize();
        }
    });
    ipcMain.on('window-close', () => {
        const win = BrowserWindow.getFocusedWindow();
        if (win) win.close();
    });
    ipcMain.on('flash-frame', () => {
        if (mainWindow && !mainWindow.isDestroyed()) mainWindow.flashFrame(true);
    });

    // Helper to fetch metadata (Zero-Knowledge: Client fetches previews)

    const OEMBED_PROVIDERS = {
        'twitter.com': (url) => url.includes('/status/') ? `https://publish.twitter.com/oembed?url=${encodeURIComponent(url)}&format=json` : null,
        'x.com': (url) => url.includes('/status/') ? `https://publish.twitter.com/oembed?url=${encodeURIComponent(url)}&format=json` : null,
        'youtube.com': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`,
        'www.youtube.com': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`,
        'youtu.be': (url) => `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`,
        'open.spotify.com': (url) => `https://open.spotify.com/oembed?url=${encodeURIComponent(url)}`,
        'www.tiktok.com': (url) => `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`,
        'tiktok.com': (url) => `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`,
        'www.reddit.com': (url) => `https://www.reddit.com/oembed?url=${encodeURIComponent(url)}&format=json`,
        'reddit.com': (url) => `https://www.reddit.com/oembed?url=${encodeURIComponent(url)}&format=json`,
    };

    const FETCH_HEADERS = {
        'User-Agent': 'Mozilla/5.0 (compatible; DiscordBot/1.0)',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.5',
    };

    const MAX_RESPONSE_SIZE = 256 * 1024; // 256KB
    const FETCH_TIMEOUT = 8000;
    const MAX_REDIRECTS = 5;

    function httpGet(url, redirectsLeft = MAX_REDIRECTS) {
        return new Promise((resolve, reject) => {
            const client = url.startsWith('https') ? https : http;
            const req = client.get(url, { headers: FETCH_HEADERS, timeout: FETCH_TIMEOUT }, (res) => {
                if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
                    if (redirectsLeft <= 0) { resolve(''); return; }
                    let redirectUrl = res.headers.location;
                    if (redirectUrl.startsWith('/')) {
                        const parsed = new URL(url);
                        redirectUrl = parsed.origin + redirectUrl;
                    }
                    res.resume();
                    httpGet(redirectUrl, redirectsLeft - 1).then(resolve).catch(reject);
                    return;
                }
                let data = '';
                let size = 0;
                res.setEncoding('utf8');
                res.on('data', (chunk) => {
                    size += Buffer.byteLength(chunk);
                    if (size > MAX_RESPONSE_SIZE) { res.destroy(); resolve(data); return; }
                    data += chunk;
                });
                res.on('end', () => resolve(data));
                res.on('error', () => resolve(data));
            });
            req.on('timeout', () => { req.destroy(); resolve(''); });
            req.on('error', (err) => { console.error('httpGet error:', err.message); resolve(''); });
        });
    }

    function fetchJson(url, redirectsLeft = MAX_REDIRECTS) {
        return new Promise((resolve, reject) => {
            const client = url.startsWith('https') ? https : http;
            const req = client.get(url, { headers: { 'User-Agent': FETCH_HEADERS['User-Agent'], 'Accept': 'application/json' }, timeout: FETCH_TIMEOUT }, (res) => {
                if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
                    if (redirectsLeft <= 0) { resolve(null); return; }
                    let redirectUrl = res.headers.location;
                    if (redirectUrl.startsWith('/')) {
                        const parsed = new URL(url);
                        redirectUrl = parsed.origin + redirectUrl;
                    }
                    res.resume();
                    fetchJson(redirectUrl, redirectsLeft - 1).then(resolve).catch(reject);
                    return;
                }
                let data = '';
                res.setEncoding('utf8');
                res.on('data', (chunk) => { data += chunk; });
                res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(null); } });
                res.on('error', () => resolve(null));
            });
            req.on('timeout', () => { req.destroy(); resolve(null); });
            req.on('error', () => resolve(null));
        });
    }

    function parseMetaTags(html) {
        const meta = {};
        const metaRegex = /<meta\s+([^>]*?)\/?\s*>/gi;
        let match;
        while ((match = metaRegex.exec(html)) !== null) {
            const attrs = match[1];
            let name = null, content = null;
            const propMatch = attrs.match(/(?:property|name)\s*=\s*["']([^"']+)["']/i);
            const contentMatch = attrs.match(/content\s*=\s*["']([^"']*?)["']/i);
            if (propMatch) name = propMatch[1].toLowerCase();
            if (contentMatch) content = contentMatch[1];
            if (name && content !== null) meta[name] = content;
        }
        const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
        if (titleMatch) meta['_title'] = titleMatch[1].trim();
        return meta;
    }

    function buildMetadata(meta) {
        const get = (...keys) => { for (const k of keys) { if (meta[k]) return meta[k]; } return null; };
        return {
            title: get('og:title', 'twitter:title', '_title'),
            description: get('og:description', 'twitter:description', 'description'),
            image: get('og:image', 'og:image:secure_url', 'twitter:image', 'twitter:image:src'),
            siteName: get('og:site_name'),
            themeColor: get('theme-color'),
            video: get('og:video:secure_url', 'og:video:url', 'og:video'),
            type: get('og:type', 'twitter:card'),
            author: get('article:author', 'author'),
        };
    }

    function sanitizeMetadata(metadata) {
        const decodeEntities = (str) => {
            if (!str) return str;
            return str.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
                .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'")
                .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(n))
                .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)));
        };
        const stripTags = (str) => str ? str.replace(/<[^>]*>/g, '') : str;
        const limit = (str, max = 1000) => str && str.length > max ? str.substring(0, max) : str;

        const result = {};
        for (const [key, value] of Object.entries(metadata)) {
            if (typeof value === 'string') {
                result[key] = limit(decodeEntities(stripTags(value)).trim());
            } else {
                result[key] = value;
            }
        }
        return result;
    }

    ipcMain.handle('fetch-metadata', async (event, url) => {
        try {
            // Check for direct video links
            const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
            const imageExtensions = ['.gif', '.png', '.jpg', '.jpeg', '.webp'];
            const lowerUrl = url.toLowerCase();

            if (videoExtensions.some(ext => lowerUrl.endsWith(ext))) {
                return { title: url.split('/').pop(), siteName: new URL(url).hostname, video: url, image: null, description: 'Video File' };
            }
            if (imageExtensions.some(ext => lowerUrl.endsWith(ext))) {
                return { title: url.split('/').pop(), siteName: new URL(url).hostname, video: null, image: url, description: 'Image File' };
            }

            const hostname = new URL(url).hostname.replace(/^www\./, '');
            const oembedBuilder = OEMBED_PROVIDERS[hostname] || OEMBED_PROVIDERS['www.' + hostname];
            const oembedUrl = oembedBuilder ? oembedBuilder(url) : null;

            let metadata;

            if (oembedUrl) {
                const [oembedResult, htmlResult] = await Promise.allSettled([
                    fetchJson(oembedUrl),
                    httpGet(url),
                ]);

                const oembed = oembedResult.status === 'fulfilled' ? oembedResult.value : null;
                const html = htmlResult.status === 'fulfilled' ? htmlResult.value : '';
                const meta = html ? parseMetaTags(html) : {};
                const ogData = buildMetadata(meta);

                metadata = {
                    title: oembed?.title || ogData.title,
                    description: ogData.description,
                    image: oembed?.thumbnail_url || ogData.image,
                    siteName: oembed?.provider_name || ogData.siteName,
                    themeColor: ogData.themeColor,
                    video: ogData.video,
                    type: ogData.type,
                    author: oembed?.author_name || ogData.author,
                };
            } else {
                const html = await httpGet(url);
                if (!html) return null;
                const meta = parseMetaTags(html);
                metadata = buildMetadata(meta);
            }

            return sanitizeMetadata(metadata);
        } catch (err) {
            console.error('Metadata fetch error:', err);
            return null;
        }
    });

    // Flatpak update check
    ipcMain.handle('check-flatpak-update', async () => {
        const isFlatpak = fs.existsSync('/.flatpak-info') || !!process.env.FLATPAK_ID;
        if (!isFlatpak) return { isFlatpak: false };

        try {
            const yaml = await httpGet('https://gitea.moyettes.com/Moyettes/DiscordClone/releases/download/latest/latest-linux.yml');
            if (!yaml) return { isFlatpak: true, updateAvailable: false };

            const versionMatch = yaml.match(/^version:\s*(.+)$/m);
            if (!versionMatch) return { isFlatpak: true, updateAvailable: false };

            const latestVersion = versionMatch[1].trim();
            const currentVersion = app.getVersion();

            const latest = latestVersion.split('.').map(Number);
            const current = currentVersion.split('.').map(Number);
            let updateAvailable = false;
            let updateType = 'patch';
            for (let i = 0; i < Math.max(latest.length, current.length); i++) {
                const l = latest[i] || 0;
                const c = current[i] || 0;
                if (l > c) {
                    updateAvailable = true;
                    updateType = i === 0 ? 'major' : i === 1 ? 'minor' : 'patch';
                    break;
                }
                if (l < c) break;
            }

            return { isFlatpak: true, updateAvailable, updateType, latestVersion, currentVersion };
        } catch (err) {
            console.error('Flatpak update check error:', err.message);
            return { isFlatpak: true, updateAvailable: false };
        }
    });

    ipcMain.handle('open-external', async (event, url) => {
        await shell.openExternal(url);
    });

    // Settings IPC handlers
    ipcMain.handle('get-setting', (event, key) => {
        const settings = loadSettings();
        return settings[key];
    });

    ipcMain.handle('set-setting', (event, key, value) => {
        const settings = loadSettings();
        settings[key] = value;
        saveSettings(settings);
    });

    // Secure session persistence handlers
    ipcMain.handle('save-session', (event, data) => {
        try {
            if (!safeStorage.isEncryptionAvailable()) return false;
            const encrypted = safeStorage.encryptString(JSON.stringify(data));
            fs.writeFileSync(SESSION_FILE, encrypted);
            return true;
        } catch (err) {
            console.error('Failed to save session:', err.message);
            return false;
        }
    });

    ipcMain.handle('load-session', () => {
        try {
            if (!safeStorage.isEncryptionAvailable()) return null;
            if (!fs.existsSync(SESSION_FILE)) return null;
            const encrypted = fs.readFileSync(SESSION_FILE);
            const decrypted = safeStorage.decryptString(encrypted);
            return JSON.parse(decrypted);
        } catch (err) {
            console.error('Failed to load session (clearing corrupt file):', err.message);
            try { fs.unlinkSync(SESSION_FILE); } catch {}
            return null;
        }
    });

    ipcMain.handle('clear-session', () => {
        try {
            if (fs.existsSync(SESSION_FILE)) fs.unlinkSync(SESSION_FILE);
            return true;
        } catch (err) {
            console.error('Failed to clear session:', err.message);
            return false;
        }
    });

    ipcMain.handle('get-screen-sources', async () => {
        const { desktopCapturer } = require('electron');
        const sources = await desktopCapturer.getSources({
            types: ['window', 'screen'],
            thumbnailSize: { width: 450, height: 250, borderRadius: '8px' },
            fetchWindowIcons: true
        });
        return sources.map(source => ({
            id: source.id,
            name: source.name,
            thumbnail: source.thumbnail.toDataURL(),
            appIcon: source.appIcon ? source.appIcon.toDataURL() : null
        }));
    });

    // Crypto Handlers
    const crypto = require('crypto');

    ipcMain.handle('generate-keys', async () => {
        const generateRSA = () => new Promise((resolve, reject) => {
            crypto.generateKeyPair('rsa', {
                modulusLength: 2048,
                publicKeyEncoding: { type: 'spki', format: 'pem' },
                privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
            }, (err, pub, priv) => {
                if (err) reject(err); else resolve({ pub, priv });
            });
        });

        const generateEd = () => new Promise((resolve, reject) => {
            crypto.generateKeyPair('ed25519', {
                publicKeyEncoding: { type: 'spki', format: 'pem' },
                privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
            }, (err, pub, priv) => {
                if (err) reject(err); else resolve({ pub, priv });
            });
        });

        const [rsa, ed] = await Promise.all([generateRSA(), generateEd()]);

        return {
            rsaPub: rsa.pub,
            rsaPriv: rsa.priv,
            edPub: ed.pub,
            edPriv: ed.priv
        };
    });

    ipcMain.handle('random-bytes', (event, size) => {
        return crypto.randomBytes(size).toString('hex');
    });

    ipcMain.handle('sha256', (event, data) => {
        return crypto.createHash('sha256').update(data).digest('hex');
    });

    ipcMain.handle('sign-message', (event, privateKeyPem, message) => {
        return crypto.sign(null, Buffer.from(message), privateKeyPem).toString('hex');
    });

    ipcMain.handle('verify-signature', (event, publicKeyPem, message, signature) => {
        return crypto.verify(null, Buffer.from(message), publicKeyPem, Buffer.from(signature, 'hex'));
    });

    ipcMain.handle('derive-auth-keys', (event, password, salt) => {
        return new Promise((resolve, reject) => {
            crypto.scrypt(password, salt, 64, (err, derivedKey) => {
                if (err) reject(err);
                else {
                    const dak = derivedKey.subarray(0, 32).toString('hex');
                    const dek = derivedKey.subarray(32, 64);
                    resolve({ dak, dek });
                }
            });
        });
    });

    ipcMain.handle('public-encrypt', (event, publicKeyPem, data) => {
        const buffer = Buffer.from(data);
        return crypto.publicEncrypt({
            key: publicKeyPem,
            padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
            oaepHash: "sha256",
        }, buffer).toString('hex');
    });

    ipcMain.handle('private-decrypt', (event, privateKeyPem, encryptedHex) => {
        const buffer = Buffer.from(encryptedHex, 'hex');
        return crypto.privateDecrypt({
            key: privateKeyPem,
            padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
            oaepHash: "sha256",
        }, buffer).toString();
    });

    ipcMain.handle('encrypt-data', (event, plaintext, key) => {
        console.log('encrypt-data called with:', {
            plaintextType: typeof plaintext,
            isBuffer: Buffer.isBuffer(plaintext),
            keyType: typeof key,
            keyIsBuffer: Buffer.isBuffer(plaintext)
        });

        if (plaintext === undefined) throw new TypeError('plaintext is undefined');
        if (key === undefined) throw new TypeError('key is undefined');

        const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
        const iv = crypto.randomBytes(12);
        const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);

        const encryptedBuffer = Buffer.concat([
            cipher.update(plaintext),
            cipher.final()
        ]);

        const tag = cipher.getAuthTag().toString('hex');
        return { content: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag };
    });

    ipcMain.handle('decrypt-data', (event, ciphertext, key, iv, tag, options = {}) => {
        const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
        const ivBuffer = Buffer.from(iv, 'hex');
        const tagBuffer = Buffer.from(tag, 'hex');
        const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
        decipher.setAuthTag(tagBuffer);

        const outputEncoding = options.encoding || 'utf8';
        const updateEncoding = outputEncoding === 'buffer' ? undefined : outputEncoding;

        let decrypted;
        if (outputEncoding === 'buffer') {
             decrypted = Buffer.concat([
                 decipher.update(ciphertext, 'hex'),
                 decipher.final()
             ]);
        } else {
             decrypted = decipher.update(ciphertext, 'hex', outputEncoding);
             decrypted += decipher.final(outputEncoding);
        }
        return decrypted;
    });

    ipcMain.handle('decrypt-batch', (event, items) => {
        return items.map(({ ciphertext, key, iv, tag }) => {
            try {
                const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
                const ivBuffer = Buffer.from(iv, 'hex');
                const tagBuffer = Buffer.from(tag, 'hex');
                const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
                decipher.setAuthTag(tagBuffer);
                let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
                decrypted += decipher.final('utf8');
                return { success: true, data: decrypted };
            } catch (err) {
                return { success: false, data: null };
            }
        });
    });

    ipcMain.handle('verify-batch', (event, items) => {
        return items.map(({ publicKey, message, signature }) => {
            try {
                const verified = crypto.verify(null, Buffer.from(message), publicKey, Buffer.from(signature, 'hex'));
                return { success: true, verified };
            } catch (err) {
                return { success: false, verified: false };
            }
        });
    });

    // --- Search DB file storage ---
    const SEARCH_DIR = path.join(app.getPath('userData'), 'search');

    ipcMain.handle('search-db-load', (event, userId) => {
        try {
            const filePath = path.join(SEARCH_DIR, `search-${userId}.db.enc`);
            if (!fs.existsSync(filePath)) return null;
            return new Uint8Array(fs.readFileSync(filePath));
        } catch (err) {
            console.error('Search DB load error:', err.message);
            return null;
        }
    });

    ipcMain.handle('search-db-save', (event, userId, data) => {
        try {
            if (!fs.existsSync(SEARCH_DIR)) fs.mkdirSync(SEARCH_DIR, { recursive: true });
            const filePath = path.join(SEARCH_DIR, `search-${userId}.db.enc`);
            fs.writeFileSync(filePath, Buffer.from(data));
            return true;
        } catch (err) {
            console.error('Search DB save error:', err.message);
            return false;
        }
    });

    ipcMain.handle('search-db-clear', (event, userId) => {
        try {
            const filePath = path.join(SEARCH_DIR, `search-${userId}.db.enc`);
            if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
            return true;
        } catch (err) {
            console.error('Search DB clear error:', err.message);
            return false;
        }
    });

    // AFK voice channel: expose system idle time to renderer
    ipcMain.handle('get-system-idle-time', () => powerMonitor.getSystemIdleTime());

    // ── Voice recording IPC ─────────────────────────────────────
    // See the module-level comment block for the session / manifest
    // model. All handlers are per-session keyed by sessionId; the
    // root directory is chosen on `start-session` and remembered
    // for the lifetime of the session so later calls can resolve
    // file paths without re-reading the user's settings.

    ipcMain.handle('recording-get-default-folder', () => DEFAULT_RECORDING_ROOT);

    ipcMain.handle('recording-pick-folder', async () => {
        const win = BrowserWindow.getFocusedWindow();
        const result = await dialog.showOpenDialog(win ?? undefined, {
            title: 'Choose recording folder',
            properties: ['openDirectory', 'createDirectory'],
            defaultPath: DEFAULT_RECORDING_ROOT,
        });
        if (result.canceled || result.filePaths.length === 0) {
            return { ok: false, path: null };
        }
        return { ok: true, path: result.filePaths[0] };
    });

    ipcMain.handle('recording-validate-folder', async (_event, dir) => {
        try {
            const ok = await isWritableDir(dir);
            return { valid: ok, error: ok ? null : 'Folder is not writable.' };
        } catch (err) {
            return { valid: false, error: err?.message ?? 'Unknown error' };
        }
    });

    ipcMain.handle('recording-open-folder', async (_event, dir) => {
        const target = dir || DEFAULT_RECORDING_ROOT;
        try {
            await fs.promises.mkdir(target, { recursive: true });
        } catch {}
        const err = await shell.openPath(target);
        return { ok: !err, error: err || null };
    });

    ipcMain.handle('recording-open-session-folder', async (_event, { rootDir, sessionId }) => {
        const root = rootDir || DEFAULT_RECORDING_ROOT;
        const sessionDir = path.join(root, sanitizeFilenameSegment(sessionId));
        const err = await shell.openPath(sessionDir);
        return { ok: !err, error: err || null };
    });

    ipcMain.handle('recording-start-session', async (_event, payload) => {
        const { sessionId, rootDir, channelId, channelName, startedAt } = payload;
        if (!sessionId) return { ok: false, error: 'missing sessionId' };
        const safeSession = sanitizeFilenameSegment(sessionId);
        const root = rootDir || DEFAULT_RECORDING_ROOT;
        const writable = await isWritableDir(root);
        const resolvedRoot = writable ? root : DEFAULT_RECORDING_ROOT;
        const sessionDir = path.join(resolvedRoot, safeSession);
        await fs.promises.mkdir(sessionDir, { recursive: true });
        const manifest = {
            sessionId,
            channelId: channelId ?? null,
            channelName: channelName ?? null,
            startedAt: startedAt ?? Date.now(),
            endedAt: null,
            participants: [],
        };
        await writeManifest(sessionDir, manifest);
        recordingSessions.set(sessionId, {
            rootDir: resolvedRoot,
            dir: sessionDir,
            streams: new Map(),
        });
        return { ok: true, rootDir: resolvedRoot, sessionDir };
    });

    ipcMain.handle('recording-open-track', async (_event, payload) => {
        const { sessionId, participantId, displayName, joinedOffsetMs } = payload;
        const session = recordingSessions.get(sessionId);
        if (!session) return { ok: false, error: 'session not started' };
        const safePart = sanitizeFilenameSegment(participantId);
        // Unique filename if the same participant rejoined mid-session.
        let baseName = safePart;
        let suffix = 1;
        let fileName = `${baseName}.webm`;
        while (session.streams.has(`${participantId}::${suffix}`) ||
            fs.existsSync(path.join(session.dir, fileName))) {
            suffix += 1;
            fileName = `${baseName}-${suffix}.webm`;
        }
        const filePath = path.join(session.dir, fileName);
        const stream = fs.createWriteStream(filePath, { flags: 'a' });
        const key = `${participantId}::${suffix}`;
        session.streams.set(key, {
            stream,
            filePath,
            fileName,
            participantId,
            chunkCount: 0,
        });
        await updateManifest(session.dir, (m) => {
            m.participants.push({
                id: participantId,
                displayName: displayName ?? null,
                file: fileName,
                joinedOffsetMs: joinedOffsetMs ?? 0,
                leftOffsetMs: null,
            });
            return m;
        });
        return { ok: true, trackKey: key, fileName };
    });

    ipcMain.handle('recording-append', async (_event, payload) => {
        const { sessionId, trackKey, chunk } = payload;
        const session = recordingSessions.get(sessionId);
        if (!session) return { ok: false, error: 'session not started' };
        const entry = session.streams.get(trackKey);
        if (!entry) return { ok: false, error: 'track not open' };
        return new Promise((resolve) => {
            entry.stream.write(Buffer.from(chunk), (err) => {
                if (err) {
                    resolve({ ok: false, error: err.message });
                    return;
                }
                entry.chunkCount += 1;
                if (entry.chunkCount % RECORDING_FSYNC_INTERVAL === 0) {
                    // Best-effort fsync on the underlying fd so that a
                    // power-cut after this return gives us up-to-date
                    // bytes on disk. The `fd` property is set once the
                    // write stream's 'open' event fires; ignore the
                    // call if it isn't ready yet.
                    const fd = entry.stream.fd;
                    if (typeof fd === 'number') {
                        fs.fsync(fd, () => {});
                    }
                }
                resolve({ ok: true });
            });
        });
    });

    ipcMain.handle('recording-close-track', async (_event, payload) => {
        const { sessionId, trackKey, leftOffsetMs } = payload;
        const session = recordingSessions.get(sessionId);
        if (!session) return { ok: false, error: 'session not started' };
        const entry = session.streams.get(trackKey);
        if (!entry) return { ok: false, error: 'track not open' };
        await new Promise((resolve) => entry.stream.end(resolve));
        session.streams.delete(trackKey);
        await updateManifest(session.dir, (m) => {
            // Match the last participant row with this id that has
            // no leftOffsetMs yet (handles rejoin entries).
            for (let i = m.participants.length - 1; i >= 0; i--) {
                if (
                    m.participants[i].id === entry.participantId &&
                    m.participants[i].file === entry.fileName &&
                    m.participants[i].leftOffsetMs == null
                ) {
                    m.participants[i].leftOffsetMs = leftOffsetMs ?? null;
                    break;
                }
            }
            return m;
        });
        return { ok: true };
    });

    ipcMain.handle('recording-finalize', async (_event, payload) => {
        const { sessionId, endedAt } = payload;
        const session = recordingSessions.get(sessionId);
        if (!session) return { ok: false, error: 'session not started' };
        // Close any stragglers.
        for (const [key, entry] of session.streams) {
            await new Promise((resolve) => entry.stream.end(resolve));
            session.streams.delete(key);
        }
        await updateManifest(session.dir, (m) => {
            m.endedAt = endedAt ?? Date.now();
            return m;
        });
        const result = { ok: true, dir: session.dir };
        recordingSessions.delete(sessionId);
        return result;
    });

    ipcMain.handle('recording-list-recoverable', async (_event, payload) => {
        const { rootDir } = payload ?? {};
        const root = rootDir || DEFAULT_RECORDING_ROOT;
        try {
            const entries = await fs.promises.readdir(root, { withFileTypes: true });
            const out = [];
            for (const entry of entries) {
                if (!entry.isDirectory()) continue;
                const sessionDir = path.join(root, entry.name);
                const manifest = await readManifest(sessionDir);
                if (!manifest) continue;
                if (manifest.endedAt != null) continue;
                // Only surface sessions that actually have at least
                // one track file — empty dirs are noise.
                const files = await fs.promises.readdir(sessionDir);
                const tracks = files.filter((f) => f.endsWith('.webm'));
                if (tracks.length === 0) continue;
                out.push({
                    sessionId: manifest.sessionId,
                    sessionDir,
                    channelId: manifest.channelId,
                    channelName: manifest.channelName,
                    startedAt: manifest.startedAt,
                    participantCount: manifest.participants?.length ?? tracks.length,
                    trackCount: tracks.length,
                });
            }
            // Newest first.
            out.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
            return { ok: true, sessions: out };
        } catch (err) {
            if (err?.code === 'ENOENT') return { ok: true, sessions: [] };
            return { ok: false, error: err?.message ?? 'unknown', sessions: [] };
        }
    });

    ipcMain.handle('recording-recover-session', async (_event, payload) => {
        // Caller already decided what to do: 'keep' stamps endedAt
        // and leaves files in place; 'delete' removes the session
        // directory entirely.
        const { sessionDir, action } = payload;
        if (!sessionDir) return { ok: false, error: 'missing sessionDir' };
        if (action === 'delete') {
            try {
                await fs.promises.rm(sessionDir, { recursive: true, force: true });
                return { ok: true };
            } catch (err) {
                return { ok: false, error: err?.message ?? 'delete failed' };
            }
        }
        // Keep: stamp endedAt to the newest mtime among track files.
        try {
            const files = await fs.promises.readdir(sessionDir);
            let newest = 0;
            for (const f of files) {
                if (!f.endsWith('.webm')) continue;
                const st = await fs.promises.stat(path.join(sessionDir, f));
                if (st.mtimeMs > newest) newest = st.mtimeMs;
            }
            await updateManifest(sessionDir, (m) => {
                m.endedAt = newest || Date.now();
                return m;
            });
            return { ok: true };
        } catch (err) {
            return { ok: false, error: err?.message ?? 'recover failed' };
        }
    });

    // --- Auto-idle detection ---
    const IDLE_THRESHOLD_SECONDS = 300; // 5 minutes
    let wasIdle = false;
    setInterval(() => {
        if (!mainWindow || mainWindow.isDestroyed()) return;
        const idleTime = powerMonitor.getSystemIdleTime();
        if (!wasIdle && idleTime >= IDLE_THRESHOLD_SECONDS) {
            wasIdle = true;
            mainWindow.webContents.send('idle-state-changed', { isIdle: true });
        } else if (wasIdle && idleTime < IDLE_THRESHOLD_SECONDS) {
            wasIdle = false;
            mainWindow.webContents.send('idle-state-changed', { isIdle: false });
        }
    }, 15000);
});
