/**
 * Shadow Spot - Main Electron Process
 */

const { app, BrowserWindow, ipcMain, Menu, Tray, nativeImage } = require('electron');
const path = require('path');
const fs = require('fs');
const { execSync, spawn } = require('child_process');
const SingboxManager = require('./singbox-manager');

// Stable Windows Application User Model ID. Must match build.appId so toasts,
// taskbar grouping, and the (future) installed shortcut all share one identity.
const APP_ID = 'uk.armodine.shadowspot';

// Check if running as administrator
function isAdmin() {
    try {
        execSync('net session', { stdio: 'ignore', windowsHide: true });
        return true;
    } catch (e) {
        return false;
    }
}

// Flag to track if we've shown the admin warning
let adminCheckDone = false;

let mainWindow = null;
let tray = null;
let singboxManager = null;
let isQuitting = false;     // true only during an intentional, graceful exit
let trayHintShown = false;  // one-time "running in tray" balloon
let isElevated = false;     // do we have admin rights? gates close-to-tray

// Tray assets (loaded once in createTray)
let greenDot = null;        // status indicator: connected
let redDot = null;          // status indicator: disconnected
let balloonIcon = null;     // logo shown in the tray notification

// Tray menu auto-reopen: the native context menu always closes on click (OS
// behavior). When connect/disconnect is triggered from the tray, we reopen the
// menu once the action settles so the user can act again without reopening it.
let pendingTrayReopen = false;
let pendingTrayReopenTimer = null;

let corePath = null;

// Single instance lock - prevent multiple instances
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
    // Another instance is running, quit this one
    app.quit();
} else {
    // Handle second instance launch - focus existing window
    app.on('second-instance', () => {
        if (mainWindow) {
            if (mainWindow.isMinimized()) mainWindow.restore();
            mainWindow.setSize(380, 720); // Force restore size
            mainWindow.show();
            mainWindow.focus();
        }
    });
}

// Get core path (called after app is ready)
function getCorePath() {
    if (!corePath) {
        const isPackaged = app.isPackaged;
        const appPath = isPackaged ? path.dirname(app.getPath('exe')) : path.join(__dirname, '..', '..');
        corePath = path.join(appPath, 'Core');
    }
    return corePath;
}

// Give Windows a real identity for this process so toast notifications show
// "ShadowSpot" + the brand icon instead of the default "Electron" + square icon.
// For an unpackaged/portable app this needs two things:
//   1) setAppUserModelId — tags this process (and its toasts) with our AUMID.
//   2) a per-user registration of that AUMID's DisplayName + IconUri, since there
//      is no installed Start-menu shortcut to carry them. Written to HKCU on every
//      launch (self-healing and portable — works on any PC, no installer needed).
function setupWindowsAppIdentity() {
    try {
        app.setAppUserModelId(APP_ID);
    } catch (e) { /* non-Windows or already set */ }

    if (process.platform !== 'win32') return;

    try {
        const iconPath = path.join(__dirname, '..', 'icons', 'icon.ico');
        const key = `HKCU\\Software\\Classes\\AppUserModelId\\${APP_ID}`;
        const run = (cmd) => execSync(cmd, { windowsHide: true, stdio: 'ignore' });
        run(`reg add "${key}" /v DisplayName /t REG_SZ /d "ShadowSpot" /f`);
        if (fs.existsSync(iconPath)) {
            run(`reg add "${key}" /v IconUri /t REG_SZ /d "${iconPath}" /f`);
        }
    } catch (e) {
        // Best-effort: a failure just means the toast falls back to the default
        // identity. Never block startup over it.
    }
}

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 380,
        height: 720,
        resizable: false,
        frame: false,
        transparent: true,
        backgroundColor: '#00000000', // Fully transparent
        icon: path.join(__dirname, '..', 'icons', 'icon.ico'),
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            navigateOnDragDrop: false,
            preload: path.join(__dirname, 'preload.js')
        }
    });

    mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));

    // Prevent Electron from navigating when files are dropped
    mainWindow.webContents.on('will-navigate', (event) => {
        event.preventDefault();
    });

    // Closing the window hides to tray instead of quitting, so an accidental
    // click on X never drops the tunnel. Real exit goes through quitApp().
    // Only the elevated (functional) instance does this — a non-elevated
    // launcher instance must quit on close, otherwise it lingers in the tray,
    // keeps the single-instance lock, and hijacks the elevation relaunch
    // (which surfaces as "Access is denied" when the core can't get admin).
    mainWindow.on('close', (event) => {
        if (!isQuitting && isElevated) {
            event.preventDefault();
            mainWindow.hide();
            if (!trayHintShown && tray) {
                trayHintShown = true;
                const opts = {
                    title: 'ShadowSpot',
                    content: (singboxManager && singboxManager.isConnected)
                        ? 'Всё ещё подключён. Управление им возможно из панели задач.'
                        : 'Свёрнуто в трей. Управление возможно из панели задач.'
                };
                if (balloonIcon && !balloonIcon.isEmpty()) {
                    opts.icon = balloonIcon;
                    opts.iconType = 'custom';
                }
                try {
                    tray.displayBalloon(opts);
                } catch (e) { /* balloons may be disabled by the OS */ }
            }
        }
    });

    createTray(); // Create tray immediately
}

function buildTrayMenu() {
    const connected = !!(singboxManager && singboxManager.isConnected);
    const loc = singboxManager && singboxManager.currentLocation;
    const statusLabel = connected
        ? `Connected${loc ? ': ' + (loc.name || loc.address) : ''}`
        : 'Disconnected';

    // Clickable toggle: green dot = connected, red dot = disconnected.
    // Connect/disconnect is delegated to the renderer so it reuses the same
    // logic and selected-location state the UI uses.
    const toggleItem = { label: statusLabel, click: () => toggleConnectionFromTray() };
    if (connected && greenDot) toggleItem.icon = greenDot;
    if (!connected && redDot) toggleItem.icon = redDot;

    return Menu.buildFromTemplate([
        toggleItem,
        { type: 'separator' },
        { label: 'Show/Hide', click: () => toggleWindow() },
        { type: 'separator' },
        { label: 'Exit', click: () => quitApp() }
    ]);
}

function createTray() {
    if (tray) return;

    const iconsDir = path.join(__dirname, '..', 'icons');
    const iconPath = path.join(iconsDir, 'icon.ico');
    const icon = fs.existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty();

    // Load tray assets once.
    const loadDot = (file) => {
        const p = path.join(iconsDir, file);
        return fs.existsSync(p) ? nativeImage.createFromPath(p).resize({ width: 16, height: 16 }) : null;
    };
    greenDot = loadDot('dot-green.png');
    redDot = loadDot('dot-red.png');
    const balloonPath = path.join(iconsDir, 'logo-round.png');
    balloonIcon = fs.existsSync(balloonPath) ? nativeImage.createFromPath(balloonPath) : null;

    tray = new Tray(icon);
    tray.setToolTip('ShadowSpot: Disconnected');
    tray.setContextMenu(buildTrayMenu());

    // Clicking the tray icon always brings the window up; the menu's Show/Hide
    // item is the explicit toggle.
    tray.on('click', () => showWindow());
    tray.on('double-click', () => showWindow());
}

// Tray status toggle → delegate to the renderer (it owns the selected location
// and the connect/disconnect UI state). The OS closes the menu on click, so we
// arm a one-shot reopen that fires once the action settles (see
// reopenTrayMenuIfPending, called from the connect/disconnect IPC handlers).
function toggleConnectionFromTray() {
    if (!mainWindow || mainWindow.isDestroyed()) return;
    pendingTrayReopen = true;
    if (pendingTrayReopenTimer) clearTimeout(pendingTrayReopenTimer);
    // Safety net: if the action never round-trips (e.g. no location selected, so
    // the renderer no-ops), drop the flag so it can't pop the menu on some
    // unrelated later action. 13s covers the longest connect attempt (12s).
    pendingTrayReopenTimer = setTimeout(() => {
        pendingTrayReopen = false;
        pendingTrayReopenTimer = null;
    }, 13000);
    mainWindow.webContents.send('tray-toggle-connection');
}

// Reopen the tray menu after a tray-initiated connect/disconnect settles, using
// the freshly rebuilt menu (updated Connected/Disconnected toggle). No-op for
// actions started from the main window's button (flag is never set there).
function reopenTrayMenuIfPending() {
    if (!pendingTrayReopen) return;
    pendingTrayReopen = false;
    if (pendingTrayReopenTimer) {
        clearTimeout(pendingTrayReopenTimer);
        pendingTrayReopenTimer = null;
    }
    if (tray) {
        try { tray.popUpContextMenu(buildTrayMenu()); } catch (e) { /* tray gone */ }
    }
}

function updateTray(connected, location) {
    if (!tray) return;
    const tip = connected
        ? `ShadowSpot: Connected${location ? ' (' + (location.name || location.address) + ')' : ''}`
        : 'ShadowSpot: Disconnected';
    tray.setToolTip(tip);
    tray.setContextMenu(buildTrayMenu());
}

function showWindow() {
    if (!mainWindow) return;
    if (mainWindow.isMinimized()) mainWindow.restore();
    mainWindow.setSize(380, 720);
    mainWindow.show();
    mainWindow.focus();
}

function toggleWindow() {
    if (!mainWindow) return;
    if (mainWindow.isVisible() && !mainWindow.isMinimized()) {
        mainWindow.hide();
    } else {
        showWindow();
    }
}

// Single graceful-exit path: disconnect the tunnel, then quit.
async function quitApp() {
    if (isQuitting) return;
    isQuitting = true;
    try {
        if (singboxManager && singboxManager.isConnected) {
            await singboxManager.disconnect();
        }
    } catch (e) { /* best effort */ }
    if (mainWindow && !mainWindow.isDestroyed()) {
        mainWindow.destroy();
    }
    app.quit();
}

// ============ IPC Handlers ============

// Get locations (dynamic, from storage)
ipcMain.handle('get-locations', () => {
    return singboxManager.getLocations();
});

// Add location from vless:// URL
ipcMain.handle('add-location', (event, vlessUrl) => {
    return singboxManager.addLocation(vlessUrl);
});

// Remove location by ID
ipcMain.handle('remove-location', (event, locationId) => {
    return singboxManager.removeLocation(locationId);
});

// Decode QR from image file path
ipcMain.handle('decode-qr', async (event, filePath) => {
    try {
        // Read image using Electron's nativeImage
        const image = nativeImage.createFromPath(filePath);

        if (image.isEmpty()) {
            return { success: false, error: 'Failed to read image file' };
        }

        // Get bitmap data
        const bitmap = image.toBitmap();
        const size = image.getSize();

        // jsQR expects Uint8ClampedArray in RGBA format
        // Electron's toBitmap() returns BGRA, need to convert to RGBA
        const rgba = new Uint8ClampedArray(bitmap.length);
        for (let i = 0; i < bitmap.length; i += 4) {
            rgba[i] = bitmap[i + 2];     // R <- B
            rgba[i + 1] = bitmap[i + 1]; // G <- G
            rgba[i + 2] = bitmap[i];     // B <- R
            rgba[i + 3] = bitmap[i + 3]; // A <- A
        }

        const jsQR = require('jsqr');
        const code = jsQR(rgba, size.width, size.height);

        if (code && code.data) {
            return { success: true, data: code.data };
        } else {
            return { success: false, error: 'No QR code found in image' };
        }
    } catch (error) {
        console.error('QR decode error:', error);
        return { success: false, error: 'Failed to decode QR: ' + error.message };
    }
});

ipcMain.handle('get-status', () => {
    return singboxManager.getStatus();
});

ipcMain.handle('connect', async (event, locationId) => {
    try {
        const result = await singboxManager.connect(locationId);
        return { success: true, location: result.location };
    } catch (error) {
        return { success: false, error: error.message };
    } finally {
        reopenTrayMenuIfPending();
    }
});

ipcMain.handle('disconnect', async () => {
    try {
        await singboxManager.disconnect();
        return { success: true };
    } catch (error) {
        return { success: false, error: error.message };
    } finally {
        reopenTrayMenuIfPending();
    }
});

ipcMain.on('window-minimize', () => {
    mainWindow.minimize();
});

ipcMain.on('window-close', () => {
    mainWindow.close();
});

// Ping handler - Clash API delay test (real RTT through proxy).
// Purely informational: it never triggers a reconnect or reroute.
ipcMain.handle('ping', async () => {
    try {
        const port = singboxManager && singboxManager.clashPort;
        if (!port) return { success: false, time: null };

        const http = require('http');
        return new Promise((resolve) => {
            const url = `http://127.0.0.1:${port}/proxies/proxy/delay?url=http%3A%2F%2Fwww.gstatic.com%2Fgenerate_204&timeout=3000`;
            const req = http.get(url, { timeout: 4000 }, (res) => {
                let body = '';
                res.on('data', (chunk) => { body += chunk; });
                res.on('end', () => {
                    try {
                        const data = JSON.parse(body);
                        if (data.delay && data.delay > 0) {
                            resolve({ success: true, time: data.delay });
                        } else {
                            resolve({ success: false, time: null });
                        }
                    } catch {
                        resolve({ success: false, time: null });
                    }
                });
            });
            req.on('error', () => resolve({ success: false, time: null }));
            req.on('timeout', () => { req.destroy(); resolve({ success: false, time: null }); });
        });
    } catch {
        return { success: false, time: null };
    }
});

// Get current server host (from the active location, no re-parsing)
ipcMain.handle('get-current-server', () => {
    if (singboxManager && singboxManager.currentLocation) {
        return singboxManager.currentLocation.address || null;
    }
    return null;
});

// Modal response handler
ipcMain.on('modal-response', async (event, response) => {
    if (response === 'exit') {
        await quitApp();
    }
    // 'cancel' just closes the modal, no action needed
});

// Elevation request handler
ipcMain.on('request-elevation', () => {
    // Handing off to an elevated relaunch: this instance must fully quit so it
    // releases the single-instance lock for the elevated copy. Mark the quit
    // as intentional so the close handler doesn't trap it into the tray.
    isQuitting = true;
    const exePath = app.getPath('exe');
    const tempDir = app.getPath('temp');
    const vbsPath = path.join(tempDir, 'elevate_shadowspot.vbs');

    const vbsContent = `
Set objShell = CreateObject("Shell.Application")
objShell.ShellExecute "${exePath.replace(/\\/g, '\\\\')}", "", "", "runas", 1
WScript.Sleep 1000
Set fso = CreateObject("Scripting.FileSystemObject")
fso.DeleteFile WScript.ScriptFullName
`;

    fs.writeFileSync(vbsPath, vbsContent.trim());

    spawn('wscript.exe', [vbsPath], {
        detached: true,
        stdio: 'ignore',
        windowsHide: true
    }).unref();

    setTimeout(() => {
        app.quit();
    }, 200);
});

// App initialization
app.whenReady().then(async () => {
    // Register our Windows app identity before any window or notification so
    // toasts attribute to "ShadowSpot" (not "Electron").
    setupWindowsAppIdentity();

    // Determine elevation up front — it gates both the admin prompt and the
    // close-to-tray behavior.
    isElevated = isAdmin();

    // Create window first
    createWindow();

    // Prompt to relaunch elevated if we don't have admin rights.
    if (!isElevated && !adminCheckDone) {
        adminCheckDone = true;
        mainWindow.webContents.on('did-finish-load', () => {
            mainWindow.webContents.send('show-modal', {
                type: 'admin',
                icon: '🔒',
                title: 'Administrator Required',
                message: 'This application requires administrator privileges to manage VPN connections. Click OK to restart with elevated permissions.',
                primaryBtn: 'OK'
            });
        });
    }

    // Pass userDataPath so locations are stored persistently
    const userDataPath = app.getPath('userData');
    singboxManager = new SingboxManager(getCorePath(), userDataPath);

    // Set up traffic callback
    singboxManager.onTraffic((traffic) => {
        if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('traffic-update', traffic);
        }
    });

    // Set up log callback
    singboxManager.onLog((message) => {
        if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('log-update', message);
        }
    });

    // Set up status callback (also reflects state in the tray)
    singboxManager.onStatusChange((status) => {
        if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('status-update', status);
        }
        updateTray(status.isConnected, status.location);
    });

    // Set up locations changed callback
    singboxManager.onLocationsChanged((locations) => {
        if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('locations-changed', locations);
        }
    });

    // Reap any orphaned core left by a previous crash before we start a new one.
    singboxManager.reapOrphan();

    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

// Safety net: if a quit is triggered while connected without going through
// quitApp(), intercept it once so the tunnel is torn down gracefully.
app.on('before-quit', (event) => {
    if (!isQuitting && singboxManager && singboxManager.isConnected) {
        event.preventDefault();
        quitApp();
    }
});
