"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserHistoryCollector = void 0;
const logger_1 = require("../logger");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const child_process_1 = require("child_process");
const sql_js_1 = __importDefault(require("sql.js"));
/**
 * Collects browser history from Chrome, Edge, and Firefox local SQLite databases.
 * Works on Windows without a browser extension by reading the browser's own history DB.
 * The DB is locked while the browser is open, so we copy it to a temp location first.
 */
class BrowserHistoryCollector {
    constructor() {
        this.lastCollectTime = Date.now() - 300000; // start 5 min ago
        this.sentUrls = new Set(); // dedup key = url+timestamp
        this.maxSentCache = 5000;
        this.sqlJsPromise = null;
    }
    /**
     * Returns an array of profile paths for Chromium-based browsers.
     * Each entry: { browser, historyPath }
     */
    getChromiumProfiles() {
        const results = [];
        if (os.platform() !== 'win32')
            return results;
        const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
        // Find all user profiles on the machine
        const usersDir = 'C:\\Users';
        let userDirs = [];
        try {
            userDirs = fs.readdirSync(usersDir).filter(d => {
                const full = path.join(usersDir, d);
                try {
                    return fs.statSync(full).isDirectory() && !['Public', 'Default', 'Default User', 'All Users'].includes(d);
                }
                catch {
                    return false;
                }
            });
        }
        catch {
            userDirs = [os.userInfo().username];
        }
        const browsers = [
            { name: 'Chrome', subPath: 'Google\\Chrome\\User Data' },
            { name: 'Edge', subPath: 'Microsoft\\Edge\\User Data' },
            { name: 'Brave', subPath: 'BraveSoftware\\Brave-Browser\\User Data' },
            { name: 'Vivaldi', subPath: 'Vivaldi\\User Data' },
            { name: 'Opera', subPath: 'Opera Software\\Opera Stable' },
        ];
        for (const user of userDirs) {
            const userLocalAppData = path.join(usersDir, user, 'AppData', 'Local');
            for (const browser of browsers) {
                const userData = path.join(userLocalAppData, browser.subPath);
                if (!fs.existsSync(userData))
                    continue;
                // Check Default profile
                const defaultHistory = path.join(userData, 'Default', 'History');
                if (fs.existsSync(defaultHistory)) {
                    results.push({ browser: browser.name, historyPath: defaultHistory });
                }
                // Check numbered profiles (Profile 1, Profile 2, etc.)
                try {
                    const entries = fs.readdirSync(userData);
                    for (const entry of entries) {
                        if (entry.startsWith('Profile ')) {
                            const profHistory = path.join(userData, entry, 'History');
                            if (fs.existsSync(profHistory)) {
                                results.push({ browser: `${browser.name} (${entry})`, historyPath: profHistory });
                            }
                        }
                    }
                }
                catch { }
            }
        }
        return results;
    }
    getFirefoxProfiles() {
        const results = [];
        if (os.platform() !== 'win32')
            return results;
        const usersDir = 'C:\\Users';
        let userDirs = [];
        try {
            userDirs = fs.readdirSync(usersDir).filter(d => {
                const full = path.join(usersDir, d);
                try {
                    return fs.statSync(full).isDirectory() && !['Public', 'Default', 'Default User', 'All Users'].includes(d);
                }
                catch {
                    return false;
                }
            });
        }
        catch {
            userDirs = [os.userInfo().username];
        }
        for (const user of userDirs) {
            const ffRoot = path.join(usersDir, user, 'AppData', 'Roaming', 'Mozilla', 'Firefox', 'Profiles');
            if (!fs.existsSync(ffRoot))
                continue;
            try {
                const profiles = fs.readdirSync(ffRoot);
                for (const prof of profiles) {
                    const placesDb = path.join(ffRoot, prof, 'places.sqlite');
                    if (fs.existsSync(placesDb)) {
                        results.push({ browser: 'Firefox', historyPath: placesDb });
                    }
                }
            }
            catch { }
        }
        return results;
    }
    /**
     * Copy browser DB to temp so we don't lock the original.
     * Returns path to temp copy or null if copy fails.
     */
    safeCopyDb(originalPath) {
        try {
            const tempDir = path.join(os.tmpdir(), 'appstats-browser');
            if (!fs.existsSync(tempDir))
                fs.mkdirSync(tempDir, { recursive: true });
            const hash = originalPath.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 80);
            const tempPath = path.join(tempDir, `${hash}.db`);
            // Use system copy to handle locked files
            if (os.platform() === 'win32') {
                // Use PowerShell Copy-Item which can handle some locks, or robocopy
                try {
                    (0, child_process_1.execSync)(`copy /Y "${originalPath}" "${tempPath}"`, {
                        stdio: 'ignore',
                        windowsHide: true,
                        shell: 'cmd.exe'
                    });
                }
                catch {
                    // Try robocopy as fallback (handles locks better)
                    try {
                        const dir = path.dirname(originalPath);
                        const file = path.basename(originalPath);
                        (0, child_process_1.execSync)(`robocopy "${dir}" "${path.dirname(tempPath)}" "${file}" /IS /IT /COPY:D /NDL /NJH /NJS /NP`, {
                            stdio: 'ignore',
                            windowsHide: true,
                            shell: 'cmd.exe'
                        });
                        // Robocopy uses the original filename — rename to our hash
                        const robocopyDest = path.join(path.dirname(tempPath), file);
                        if (fs.existsSync(robocopyDest) && robocopyDest !== tempPath) {
                            try {
                                fs.unlinkSync(tempPath);
                            }
                            catch { }
                            fs.renameSync(robocopyDest, tempPath);
                        }
                    }
                    catch {
                        return null;
                    }
                }
            }
            else {
                fs.copyFileSync(originalPath, tempPath);
            }
            return fs.existsSync(tempPath) ? tempPath : null;
        }
        catch (e) {
            return null;
        }
    }
    normalizeVisitDurationSeconds(rawValue) {
        const raw = Number(rawValue || 0);
        if (!Number.isFinite(raw) || raw <= 0)
            return 1;
        let seconds = raw;
        if (seconds >= 1000000) {
            seconds = seconds / 1000000;
        }
        else if (seconds >= 10000 || seconds > 300) {
            seconds = seconds / 1000;
        }
        if (!Number.isFinite(seconds) || seconds <= 0)
            return 1;
        return Math.max(1, Math.min(300, Math.round(seconds)));
    }
    /**
     * Query Chromium history database (Chrome, Edge, Brave, etc.)
     */
    async queryChromiumHistory(dbPath, browser, sinceMs) {
        const results = [];
        const tempDb = this.safeCopyDb(dbPath);
        if (!tempDb)
            return results;
        try {
            // Chromium stores time as microseconds since Jan 1, 1601
            // Convert JS timestamp to Chromium timestamp
            const chromiumEpochOffset = 11644473600000000; // microseconds between 1601 and 1970
            const sinceChromium = (sinceMs * 1000) + chromiumEpochOffset;
            // Use PowerShell with System.Data.SQLite or sqlite3 command
            const query = `
        SELECT u.url, u.title, v.visit_time, v.visit_duration, u.visit_count
        FROM visits v
        JOIN urls u ON v.url = u.id
        WHERE v.visit_time > ${sinceChromium}
        ORDER BY v.visit_time DESC
        LIMIT 500;
      `.replace(/\n/g, ' ').trim();
            const output = await this.runSqliteQuery(tempDb, query);
            if (!output)
                return results;
            for (const line of output.split('\n')) {
                const parts = line.trim().split('|');
                if (parts.length < 5)
                    continue;
                const [url, title, visitTimeStr, durationStr, visitCountStr] = parts;
                if (!url || url.startsWith('chrome://') || url.startsWith('edge://') || url.startsWith('about:') || url.startsWith('chrome-extension://'))
                    continue;
                const visitTimeMicro = parseInt(visitTimeStr);
                if (isNaN(visitTimeMicro))
                    continue;
                // Convert Chromium timestamp to JS Date
                const visitTimeMs = (visitTimeMicro - chromiumEpochOffset) / 1000;
                const visitDate = new Date(visitTimeMs);
                let domain = '';
                try {
                    domain = new URL(url).hostname;
                }
                catch { }
                const key = `${url}|${visitTimeStr}`;
                if (this.sentUrls.has(key))
                    continue;
                results.push({
                    url,
                    title: title || '',
                    domain,
                    visitTime: visitDate.toISOString(),
                    visitDuration: this.normalizeVisitDurationSeconds(durationStr),
                    browser,
                    visitCount: parseInt(visitCountStr) || 1,
                });
            }
        }
        catch (e) {
            logger_1.logger.debug(`Chromium history query error: ${e.message}`);
        }
        finally {
            try {
                fs.unlinkSync(tempDb);
            }
            catch { }
        }
        return results;
    }
    /**
     * Query Firefox places.sqlite
     */
    async queryFirefoxHistory(dbPath, sinceMs) {
        const results = [];
        const tempDb = this.safeCopyDb(dbPath);
        if (!tempDb)
            return results;
        try {
            // Firefox stores time as microseconds since epoch (1970)
            const sinceFirefox = sinceMs * 1000;
            const query = `
        SELECT p.url, p.title, h.visit_date, p.visit_count
        FROM moz_historyvisits h
        JOIN moz_places p ON h.place_id = p.id
        WHERE h.visit_date > ${sinceFirefox}
        ORDER BY h.visit_date DESC
        LIMIT 500;
      `.replace(/\n/g, ' ').trim();
            const output = await this.runSqliteQuery(tempDb, query);
            if (!output)
                return results;
            for (const line of output.split('\n')) {
                const parts = line.trim().split('|');
                if (parts.length < 4)
                    continue;
                const [url, title, visitDateStr, visitCountStr] = parts;
                if (!url || url.startsWith('about:') || url.startsWith('moz-extension://'))
                    continue;
                const visitTimeMicro = parseInt(visitDateStr);
                if (isNaN(visitTimeMicro))
                    continue;
                const visitDate = new Date(visitTimeMicro / 1000);
                let domain = '';
                try {
                    domain = new URL(url).hostname;
                }
                catch { }
                const key = `${url}|${visitDateStr}`;
                if (this.sentUrls.has(key))
                    continue;
                results.push({
                    url,
                    title: title || '',
                    domain,
                    visitTime: visitDate.toISOString(),
                    visitDuration: 1,
                    browser: 'Firefox',
                    visitCount: parseInt(visitCountStr) || 1,
                });
            }
        }
        catch (e) {
            logger_1.logger.debug(`Firefox history query error: ${e.message}`);
        }
        finally {
            try {
                fs.unlinkSync(tempDb);
            }
            catch { }
        }
        return results;
    }
    /**
     * Run a SQLite query using the sqlite3 command-line tool or PowerShell.
     */
    async getSqlJs() {
        if (!this.sqlJsPromise) {
            this.sqlJsPromise = (0, sql_js_1.default)({
                locateFile: (file) => {
                    try {
                        return require.resolve(`sql.js/dist/${file}`);
                    }
                    catch {
                        return file;
                    }
                },
            }).catch((e) => {
                logger_1.logger.debug(`sql.js init failed: ${e?.message || e}`);
                return null;
            });
        }
        return this.sqlJsPromise;
    }
    async runSqliteQuery(dbPath, query) {
        // Try using sqlite3 CLI (often available)
        try {
            const result = (0, child_process_1.execSync)(`sqlite3 -separator "|" "${dbPath}" "${query}"`, {
                timeout: 10000,
                windowsHide: true,
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
            });
            return result;
        }
        catch { }
        // Fallback: use PowerShell with ADO.NET
        if (os.platform() === 'win32') {
            try {
                const psQuery = query.replace(/'/g, "''");
                const ps = `
$conn = New-Object System.Data.SQLite.SQLiteConnection("Data Source='${dbPath.replace(/'/g, "''")}';Read Only=True;");
try { $conn.Open() } catch { exit 1 }
$cmd = $conn.CreateCommand();
$cmd.CommandText = '${psQuery}';
$reader = $cmd.ExecuteReader();
while ($reader.Read()) {
  $vals = @();
  for ($i=0; $i -lt $reader.FieldCount; $i++) { $vals += $reader.GetValue($i).ToString() }
  $vals -join '|'
}
$conn.Close()
        `.trim();
                const result = (0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -Command "${ps.replace(/"/g, '\\"')}"`, {
                    timeout: 10000,
                    windowsHide: true,
                    encoding: 'utf-8',
                    stdio: ['pipe', 'pipe', 'pipe'],
                });
                return result;
            }
            catch { }
        }
        // Final fallback: sql.js (no external sqlite binary required)
        try {
            const SQL = await this.getSqlJs();
            if (!SQL)
                return null;
            const dbBuffer = fs.readFileSync(dbPath);
            const db = new SQL.Database(new Uint8Array(dbBuffer));
            try {
                const rows = db.exec(query);
                if (!rows || rows.length === 0)
                    return '';
                return rows
                    .flatMap((r) => r.values)
                    .map((row) => row.map(v => (v === null || v === undefined) ? '' : String(v).replace(/\|/g, ' ')).join('|'))
                    .join('\n');
            }
            finally {
                db.close();
            }
        }
        catch (e) {
            logger_1.logger.debug(`sql.js query failed: ${e?.message || e}`);
        }
        return null;
    }
    /**
     * Collect recent browser history entries since last collection.
     */
    async collect() {
        const allEntries = [];
        try {
            const chromiumProfiles = this.getChromiumProfiles();
            const firefoxProfiles = this.getFirefoxProfiles();
            logger_1.logger.debug(`Browser history: Found ${chromiumProfiles.length} Chromium profiles, ${firefoxProfiles.length} Firefox profiles`);
            for (const profile of chromiumProfiles) {
                try {
                    const entries = await this.queryChromiumHistory(profile.historyPath, profile.browser, this.lastCollectTime);
                    allEntries.push(...entries);
                }
                catch (e) {
                    logger_1.logger.debug(`Error reading ${profile.browser} history: ${e.message}`);
                }
            }
            for (const profile of firefoxProfiles) {
                try {
                    const entries = await this.queryFirefoxHistory(profile.historyPath, this.lastCollectTime);
                    allEntries.push(...entries);
                }
                catch (e) {
                    logger_1.logger.debug(`Error reading Firefox history: ${e.message}`);
                }
            }
            // Mark sent URLs for dedup
            for (const entry of allEntries) {
                const key = `${entry.url}|${entry.visitTime}`;
                this.sentUrls.add(key);
            }
            // Prune sentUrls cache
            if (this.sentUrls.size > this.maxSentCache) {
                const arr = Array.from(this.sentUrls);
                this.sentUrls = new Set(arr.slice(arr.length - Math.floor(this.maxSentCache / 2)));
            }
            this.lastCollectTime = Date.now();
            if (allEntries.length > 0) {
                logger_1.logger.info(`Collected ${allEntries.length} browser history entries`);
            }
        }
        catch (e) {
            logger_1.logger.error(`Browser history collection error: ${e.message}`);
        }
        return allEntries;
    }
}
exports.BrowserHistoryCollector = BrowserHistoryCollector;
//# sourceMappingURL=browser-history.js.map