"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const node_path_1 = require("node:path");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_module_1 = require("node:module");
const currency_1 = require("./currency");
const data_1 = require("./data");
const i18n_1 = require("./i18n");
const settings_1 = require("./settings");
const ui_1 = require("./ui");
const nodeRequire = (0, node_module_1.createRequire)(__filename);
const Database = nodeRequire('better-sqlite3');
const DB_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage', 'cache.db');
const PORT_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage', '.serve-port');
const FX_CACHE_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage', 'widget-exchange-rate.json');
const DASHBOARD_PORT = 3847;
const WINDOW_WIDTH = 380;
const DEFAULT_WINDOW_HEIGHT = 500;
const MIN_WINDOW_HEIGHT = 320;
const FX_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
let tray = null;
let win = null;
let db = null;
let refreshTimer = null;
let positionRetryTimers = [];
let settings = (0, settings_1.loadSettings)();
let exchangeRate = loadExchangeRateCache();
let exchangeRatePromise = null;
electron_1.app.setName('AIUsage Widget');
// Prevent dock icon on macOS
if (process.platform === 'darwin' && electron_1.app.dock) {
    electron_1.app.dock.hide();
}
electron_1.app.whenReady().then(() => {
    if ((0, node_fs_1.existsSync)(DB_PATH)) {
        db = new Database(DB_PATH, {
            readonly: true,
            nativeBinding: (0, ui_1.getWidgetNativeBindingPath)(__dirname),
        });
    }
    applyTheme(settings.theme);
    createTray();
    createWindow();
    startAutoRefresh();
    void refreshExchangeRate();
    if ((0, ui_1.shouldShowWindowOnLaunch)(electron_1.app.isPackaged)) {
        showWindowWhenTrayReady();
    }
});
electron_1.app.on('window-all-closed', () => {
    // Keep the app running in the tray — do not quit
});
electron_1.app.on('before-quit', () => {
    db?.close();
});
function applyTheme(theme) {
    electron_1.nativeTheme.themeSource = theme;
}
function createTray() {
    const { buffer, scaleFactor } = (0, ui_1.getTrayIconNativeImage)();
    const icon = electron_1.nativeImage.createFromBuffer(buffer, scaleFactor ? { scaleFactor } : undefined);
    tray = new electron_1.Tray(icon);
    tray.setToolTip('AIUsage Widget');
    tray.on('click', () => toggleWindow());
    tray.on('right-click', () => {
        const i18n = (0, i18n_1.t)(settings.locale);
        const menu = electron_1.Menu.buildFromTemplate([
            { label: i18n.showPanel, click: () => showWindow() },
            { label: i18n.openDashboard, click: () => openDashboardAction() },
            { label: i18n.refresh, click: () => pushDataUpdate() },
            { type: 'separator' },
            { label: i18n.quit, click: () => { electron_1.app.exit(0); } },
        ]);
        tray.popUpContextMenu(menu);
    });
}
function createWindow() {
    win = new electron_1.BrowserWindow({
        width: WINDOW_WIDTH,
        height: DEFAULT_WINDOW_HEIGHT,
        show: false,
        frame: false,
        resizable: false,
        skipTaskbar: true,
        alwaysOnTop: true,
        transparent: true,
        webPreferences: {
            preload: (0, node_path_1.join)(__dirname, 'preload.js'),
            contextIsolation: true,
            nodeIntegration: false,
        },
    });
    const rendererPath = (0, node_path_1.join)(__dirname, 'renderer', 'index.html');
    win.loadFile(rendererPath);
    if ((0, ui_1.shouldHideWindowOnBlur)(electron_1.app.isPackaged)) {
        win.on('blur', () => win?.hide());
    }
}
function showWindow() {
    if (!win)
        return;
    positionWindowNearTray();
    win.show();
    win.focus();
    pushDataUpdate();
    schedulePositionRetries();
}
function showWindowWhenTrayReady(attempt = 0) {
    if (!tray)
        return;
    const trayBounds = tray.getBounds();
    if ((0, ui_1.hasUsableTrayBounds)({
        platform: process.platform,
        trayBounds,
        displayBounds: electron_1.screen.getDisplayNearestPoint({ x: trayBounds.x, y: trayBounds.y }).workArea,
    }) || attempt >= 12) {
        showWindow();
        return;
    }
    setTimeout(() => showWindowWhenTrayReady(attempt + 1), 80);
}
function positionWindowNearTray() {
    if (!win || !tray)
        return false;
    const trayBounds = tray.getBounds();
    const displayBounds = electron_1.screen.getDisplayNearestPoint({ x: trayBounds.x, y: trayBounds.y }).workArea;
    const winBounds = win.getBounds();
    const { x, y } = (0, ui_1.getWindowPosition)({
        platform: process.platform,
        trayBounds,
        windowBounds: winBounds,
        displayBounds,
    });
    win.setPosition(x, y, false);
    return (0, ui_1.hasUsableTrayBounds)({ platform: process.platform, trayBounds, displayBounds });
}
function schedulePositionRetries() {
    for (const timer of positionRetryTimers)
        clearTimeout(timer);
    positionRetryTimers = [80, 200, 500, 1000].map((delay) => setTimeout(() => {
        if (!win?.isVisible())
            return;
        const positionedWithRealTrayBounds = positionWindowNearTray();
        if (positionedWithRealTrayBounds) {
            for (const timer of positionRetryTimers)
                clearTimeout(timer);
            positionRetryTimers = [];
        }
    }, delay));
}
function toggleWindow() {
    if (win?.isVisible()) {
        win.hide();
    }
    else {
        showWindow();
    }
}
function pushDataUpdate() {
    if (!win || !db)
        return;
    try {
        const data = (0, data_1.queryWidgetData)(db, settings.rangeDays);
        win.webContents.send('widget:data-update', data);
    }
    catch {
        // DB may not be initialized yet; silently skip
    }
}
function startAutoRefresh() {
    if (refreshTimer)
        clearInterval(refreshTimer);
    refreshTimer = setInterval(() => pushDataUpdate(), settings.refreshIntervalSec * 1000);
}
async function openDashboardAction() {
    const port = getDashboardPort();
    const reachable = await isDashboardReachable(port);
    if (!reachable) {
        // Show widget window so user can see install progress
        showWindow();
        notifyRenderer('install:status', { phase: 'installing' });
        const result = await launchDashboard();
        if (result.success) {
            notifyRenderer('install:status', { phase: 'done' });
        }
        if (!result.success) {
            // CLI not found; attempt auto-install
            const installResult = await installAiusageCli();
            if (!installResult.success) {
                notifyRenderer('install:status', { phase: 'failed', error: installResult.error });
                electron_1.dialog.showErrorBox('Installation Failed', `Could not install @juliantanx/aiusage automatically.\n\n${installResult.error ?? 'Unknown error'}\n\nTry manually:\n  npm install -g @juliantanx/aiusage`);
                return;
            }
            notifyRenderer('install:status', { phase: 'launching' });
            const retryResult = await launchDashboard();
            if (!retryResult.success) {
                notifyRenderer('install:status', { phase: 'failed', error: retryResult.error });
                electron_1.dialog.showErrorBox('Launch Failed', 'AIUsage was installed but the dashboard failed to start.\n\nTry running:\n  aiusage serve');
                return;
            }
            notifyRenderer('install:status', { phase: 'done' });
        }
    }
    electron_1.shell.openExternal(`http://localhost:${getDashboardPort()}`);
}
function notifyRenderer(channel, payload) {
    if (win && !win.isDestroyed()) {
        win.webContents.send(channel, payload);
    }
}
function loadExchangeRateCache() {
    try {
        if ((0, node_fs_1.existsSync)(FX_CACHE_FILE)) {
            const raw = JSON.parse((0, node_fs_1.readFileSync)(FX_CACHE_FILE, 'utf-8'));
            if (raw.base === 'USD' && raw.target === 'CNY' && typeof raw.rate === 'number') {
                return raw;
            }
        }
    }
    catch {
        // Fall through to empty state
    }
    return {
        base: 'USD',
        target: 'CNY',
        rate: null,
        fetchedAt: null,
        source: currency_1.EXCHANGE_RATE_SOURCE,
    };
}
function saveExchangeRateCache(rate) {
    const dir = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage');
    if (!(0, node_fs_1.existsSync)(dir)) {
        (0, node_fs_1.mkdirSync)(dir, { recursive: true });
    }
    (0, node_fs_1.writeFileSync)(FX_CACHE_FILE, JSON.stringify(rate, null, 2), 'utf-8');
}
async function refreshExchangeRate(force = false) {
    const hasFreshRate = exchangeRate.rate !== null &&
        exchangeRate.fetchedAt !== null &&
        Date.now() - exchangeRate.fetchedAt < FX_CACHE_TTL_MS;
    if (!force && hasFreshRate)
        return exchangeRate;
    if (exchangeRatePromise)
        return exchangeRatePromise;
    exchangeRatePromise = (async () => {
        try {
            const response = await fetch(currency_1.EXCHANGE_RATE_SOURCE);
            if (!response.ok)
                throw new Error(`HTTP ${response.status}`);
            const payload = await response.json();
            const rate = payload.usd?.cny;
            if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
                throw new Error('USD/CNY rate missing');
            }
            exchangeRate = {
                base: 'USD',
                target: 'CNY',
                rate,
                fetchedAt: Date.now(),
                date: payload.date,
                source: currency_1.EXCHANGE_RATE_SOURCE,
            };
            saveExchangeRateCache(exchangeRate);
        }
        catch (error) {
            exchangeRate = {
                ...exchangeRate,
                error: error instanceof Error ? error.message : 'Failed to fetch exchange rate',
                source: currency_1.EXCHANGE_RATE_SOURCE,
            };
        }
        finally {
            exchangeRatePromise = null;
        }
        return exchangeRate;
    })();
    return exchangeRatePromise;
}
async function installAiusageCli() {
    const { execFile } = nodeRequire('child_process');
    // Try npm first, fall back to pnpm, then yarn
    const managers = ['npm', 'pnpm', 'yarn'];
    for (const pm of managers) {
        const args = pm === 'yarn'
            ? ['global', 'add', '@juliantanx/aiusage']
            : ['install', '-g', '@juliantanx/aiusage'];
        const result = await new Promise((resolve) => {
            execFile(pm, args, { timeout: 120000, shell: true }, (err, _stdout, stderr) => {
                if (err) {
                    resolve({ success: false, error: stderr || err.message });
                }
                else {
                    resolve({ success: true });
                }
            });
        });
        if (result.success)
            return result;
        // If this package manager isn't installed, try the next one
    }
    return { success: false, error: 'No package manager (npm/pnpm/yarn) could install @juliantanx/aiusage.' };
}
// IPC handlers
electron_1.ipcMain.handle('widget:get-data', () => {
    if (!db)
        return null;
    return (0, data_1.queryWidgetData)(db);
});
electron_1.ipcMain.handle('widget:open-dashboard', async () => {
    await openDashboardAction();
});
electron_1.ipcMain.handle('widget:get-settings', () => {
    return settings;
});
electron_1.ipcMain.handle('widget:get-exchange-rate', async () => {
    return refreshExchangeRate();
});
electron_1.ipcMain.handle('widget:save-settings', (_event, newSettings) => {
    settings = newSettings;
    (0, settings_1.saveSettings)(settings);
    applyTheme(settings.theme);
    startAutoRefresh();
    return settings;
});
electron_1.ipcMain.on('widget:hide-window', () => {
    win?.hide();
});
electron_1.ipcMain.on('widget:resize-window', (_event, height) => {
    if (!win || !Number.isFinite(height))
        return;
    const bounds = win.getBounds();
    const displayBounds = electron_1.screen.getDisplayNearestPoint({ x: bounds.x, y: bounds.y }).workArea;
    const nextHeight = Math.min(Math.max(Math.ceil(height), MIN_WINDOW_HEIGHT), displayBounds.height);
    if (Math.abs(bounds.height - nextHeight) < 2)
        return;
    win.setSize(WINDOW_WIDTH, nextHeight, false);
    if (win.isVisible()) {
        positionWindowNearTray();
    }
});
function getDashboardPort() {
    try {
        if ((0, node_fs_1.existsSync)(PORT_FILE)) {
            const port = parseInt((0, node_fs_1.readFileSync)(PORT_FILE, 'utf-8').trim(), 10);
            if (!isNaN(port) && port > 0)
                return port;
        }
    }
    catch { }
    return DASHBOARD_PORT;
}
async function isDashboardReachable(port) {
    return new Promise((resolve) => {
        const http = nodeRequire('http');
        const req = http.get(`http://localhost:${port}`, (res) => {
            res.destroy();
            resolve(res.statusCode !== undefined && res.statusCode < 500);
        });
        req.on('error', () => resolve(false));
        req.setTimeout(200, () => { req.destroy(); resolve(false); });
    });
}
async function launchDashboard() {
    const { spawn } = nodeRequire('child_process');
    return new Promise((resolve) => {
        const child = spawn('aiusage', ['serve'], {
            detached: true,
            stdio: 'ignore',
            shell: true,
        });
        let failed = false;
        child.on('error', () => { failed = true; });
        child.on('close', (code) => {
            if (code !== 0)
                failed = true;
        });
        child.unref();
        let attempts = 0;
        const check = async () => {
            if (failed) {
                resolve({ success: false, error: 'aiusage command not found' });
                return;
            }
            if (await isDashboardReachable(getDashboardPort())) {
                resolve({ success: true });
                return;
            }
            attempts++;
            if (attempts >= 25) {
                resolve({ success: false, error: 'Server failed to start within 5 seconds' });
                return;
            }
            setTimeout(check, 200);
        };
        check();
    });
}
