/**
 * sqlite3 - SQLite database CLI
 *
 * Wraps sql.js (WASM) to provide SQLite database access through the virtual filesystem.
 * Databases are loaded from buffers and written back after modifications.
 *
 * Queries run in a worker thread with a timeout to prevent runaway queries
 * (e.g., infinite recursive CTEs) from blocking execution.
 *
 * Security: sql.js is fully sandboxed - it cannot access the real filesystem,
 * making ATTACH DATABASE and VACUUM INTO safe (they only operate on virtual buffers).
 */
import { randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Worker } from "node:worker_threads";
import initSqlJs from "sql.js";
import { sanitizeErrorMessage, sanitizeHostErrorMessage, } from "../../fs/sanitize-error.js";
import { bindDefenseContextCallback } from "../../security/defense-context.js";
import { DefenseInDepthBox } from "../../security/defense-in-depth-box.js";
import { _clearTimeout, _setTimeout } from "../../timers.js";
import { hasHelpFlag, showHelp } from "../help.js";
import { formatOutput, } from "./formatters.js";
/** Default query timeout in milliseconds (5 seconds) */
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
const sqlite3Help = {
    name: "sqlite3",
    summary: "SQLite database CLI",
    usage: "sqlite3 [OPTIONS] DATABASE [SQL]",
    options: [
        "-list           output in list mode (default)",
        "-csv            output in CSV mode",
        "-json           output in JSON mode",
        "-line           output in line mode",
        "-column         output in column mode",
        "-table          output as ASCII table",
        "-markdown       output as markdown table",
        "-tabs           output in tab-separated mode",
        "-box            output in Unicode box mode",
        "-quote          output in SQL quote mode",
        "-html           output as HTML table",
        "-ascii          output in ASCII mode (control chars)",
        "-header         show column headers",
        "-noheader       hide column headers",
        "-separator SEP  field separator for list mode (default: |)",
        "-newline SEP    row separator (default: \\n)",
        "-nullvalue TEXT text for NULL values (default: empty)",
        "-readonly       open database read-only (no writeback)",
        "-bail           stop on first error",
        "-echo           print SQL before execution",
        "-cmd COMMAND    run SQL command before main SQL",
        "-version        show SQLite version",
        "--              end of options",
        "--help          show this help",
    ],
    examples: [
        'sqlite3 :memory: "CREATE TABLE t(x); INSERT INTO t VALUES(1); SELECT * FROM t"',
        'sqlite3 -json data.db "SELECT * FROM users"',
        'sqlite3 -csv -header data.db "SELECT id, name FROM products"',
        'sqlite3 -box data.db "SELECT * FROM users"',
    ],
};
function parseArgs(args) {
    const options = {
        mode: "list",
        header: false,
        separator: "|",
        newline: "\n",
        nullValue: "",
        readonly: false,
        bail: false,
        echo: false,
        cmd: null,
    };
    let database = null;
    let sql = null;
    let showVersion = false;
    let endOfOptions = false;
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        // After --, treat everything as positional arguments
        if (endOfOptions) {
            if (database === null) {
                database = arg;
            }
            else if (sql === null) {
                sql = arg;
            }
            continue;
        }
        if (arg === "--") {
            endOfOptions = true;
        }
        else if (arg === "-version") {
            showVersion = true;
        }
        else if (arg === "-list")
            options.mode = "list";
        else if (arg === "-csv")
            options.mode = "csv";
        else if (arg === "-json")
            options.mode = "json";
        else if (arg === "-line")
            options.mode = "line";
        else if (arg === "-column")
            options.mode = "column";
        else if (arg === "-table")
            options.mode = "table";
        else if (arg === "-markdown")
            options.mode = "markdown";
        else if (arg === "-tabs")
            options.mode = "tabs";
        else if (arg === "-box")
            options.mode = "box";
        else if (arg === "-quote")
            options.mode = "quote";
        else if (arg === "-html")
            options.mode = "html";
        else if (arg === "-ascii")
            options.mode = "ascii";
        else if (arg === "-header")
            options.header = true;
        else if (arg === "-noheader")
            options.header = false;
        else if (arg === "-readonly")
            options.readonly = true;
        else if (arg === "-bail")
            options.bail = true;
        else if (arg === "-echo")
            options.echo = true;
        else if (arg === "-separator") {
            if (i + 1 >= args.length) {
                return {
                    stdout: "",
                    stderr: "sqlite3: Error: missing argument to -separator\n",
                    exitCode: 1,
                };
            }
            options.separator = args[++i];
        }
        else if (arg === "-newline") {
            if (i + 1 >= args.length) {
                return {
                    stdout: "",
                    stderr: "sqlite3: Error: missing argument to -newline\n",
                    exitCode: 1,
                };
            }
            options.newline = args[++i];
        }
        else if (arg === "-nullvalue") {
            if (i + 1 >= args.length) {
                return {
                    stdout: "",
                    stderr: "sqlite3: Error: missing argument to -nullvalue\n",
                    exitCode: 1,
                };
            }
            options.nullValue = args[++i];
        }
        else if (arg === "-cmd") {
            if (i + 1 >= args.length) {
                return {
                    stdout: "",
                    stderr: "sqlite3: Error: missing argument to -cmd\n",
                    exitCode: 1,
                };
            }
            options.cmd = args[++i];
        }
        else if (arg.startsWith("-")) {
            // Real sqlite3 treats --xyz as -xyz and says "unknown option: -xyz"
            const optName = arg.startsWith("--") ? arg.slice(1) : arg;
            return {
                stdout: "",
                stderr: `sqlite3: Error: unknown option: ${optName}\nUse -help for a list of options.\n`,
                exitCode: 1,
            };
        }
        else if (database === null) {
            database = arg;
        }
        else if (sql === null) {
            sql = arg;
        }
    }
    return { options, database, sql, showVersion };
}
// Get SQLite version from sql.js
async function getSqliteVersion() {
    const SQL = await DefenseInDepthBox.runTrustedAsync(() => initSqlJs());
    const db = new SQL.Database();
    try {
        const result = db.exec("SELECT sqlite_version()");
        if (result.length > 0 && result[0].values.length > 0) {
            return String(result[0].values[0][0]);
        }
        return "unknown";
    }
    finally {
        db.close();
    }
}
/**
 * Find the sqlite3 worker.js file path.
 * Checks multiple locations for different environments:
 * - dist/commands/sqlite3/worker.js (production, bundled)
 * - ./worker.js (development from dist/)
 * - ../../../dist/commands/sqlite3/worker.js (tests from src/)
 */
function findWorkerPath() {
    let _workerPath = "worker.js";
    const isNode = typeof process !== "undefined";
    const hasImportMeta = typeof import.meta !== "undefined";
    if (hasImportMeta && import.meta.url) {
        try {
            const url = new URL(import.meta.url);
            if (url.protocol === "file:") {
                const baseDir = dirname(fileURLToPath(url));
                const localPath = join(baseDir, "worker.js");
                if (isNode) {
                    const paths = [
                        localPath,
                        join(baseDir, "sqlite-worker.js"),
                        join(baseDir, "chunks", "sqlite-worker.js"),
                        join(baseDir, "..", "commands", "sqlite3", "worker.js"),
                        join(baseDir, "../../../dist/commands/sqlite3/worker.js"),
                    ];
                    for (const p of paths) {
                        if (existsSync(p)) {
                            return p;
                        }
                    }
                }
                _workerPath = localPath;
            }
        }
        catch {
            // ignore
        }
    }
    return _workerPath;
}
/** @internal Exposed for testing only */
export const _internals = {
    createWorker(workerPath, input) {
        return new Worker(workerPath, { workerData: input });
    },
};
function generateWorkerProtocolToken() {
    return randomBytes(16).toString("hex");
}
function normalizeWorkerResult(result, expectedProtocolToken) {
    if (!result || typeof result !== "object") {
        return {
            success: false,
            error: "Malformed worker response",
        };
    }
    const raw = result;
    if (typeof raw.protocolToken !== "string" ||
        raw.protocolToken !== expectedProtocolToken) {
        return {
            success: false,
            error: "Malformed worker response: invalid protocol token",
        };
    }
    if (raw.type === "security-violation") {
        return {
            success: false,
            error: `Security violation: ${typeof raw.violation?.type === "string" ? raw.violation.type : "unknown"}`,
        };
    }
    if (typeof raw.success !== "boolean") {
        return {
            success: false,
            error: "Malformed worker response: missing success flag",
        };
    }
    if (!raw.success) {
        return {
            success: false,
            error: typeof raw.error === "string" && raw.error.length > 0
                ? raw.error
                : "Worker execution failed",
        };
    }
    if (!Array.isArray(raw.results)) {
        return {
            success: false,
            error: "Malformed worker response: missing results array",
        };
    }
    if (typeof raw.hasModifications !== "boolean") {
        return {
            success: false,
            error: "Malformed worker response: missing hasModifications flag",
        };
    }
    if (raw.dbBuffer !== null && raw.dbBuffer !== undefined) {
        if (!(raw.dbBuffer instanceof Uint8Array)) {
            return {
                success: false,
                error: "Malformed worker response: invalid dbBuffer",
            };
        }
    }
    return {
        success: true,
        results: raw.results,
        hasModifications: raw.hasModifications,
        dbBuffer: raw.dbBuffer === undefined
            ? null
            : raw.dbBuffer,
        defenseStats: raw.defenseStats,
    };
}
async function executeInWorker(input, timeoutMs, requireDefenseContext) {
    // Try to use worker thread for timeout protection
    try {
        const workerPath = findWorkerPath();
        return await new Promise((resolve, reject) => {
            const worker = DefenseInDepthBox.runTrusted(() => _internals.createWorker(workerPath, input));
            const onTimeout = bindDefenseContextCallback(requireDefenseContext, "sqlite3", "worker timeout callback", () => {
                worker.terminate();
                resolve({
                    success: false,
                    error: `Query timeout: execution exceeded ${timeoutMs}ms limit`,
                });
            });
            const dispatchTimeout = () => {
                try {
                    onTimeout();
                }
                catch (error) {
                    const message = error instanceof Error ? error.message : String(error);
                    resolve({
                        success: false,
                        error: sanitizeHostErrorMessage(message),
                    });
                }
            };
            const timeout = _setTimeout(dispatchTimeout, timeoutMs);
            const onMessage = bindDefenseContextCallback(requireDefenseContext, "sqlite3", "worker message callback", (result) => {
                _clearTimeout(timeout);
                resolve(normalizeWorkerResult(result, input.protocolToken));
            });
            const onError = bindDefenseContextCallback(requireDefenseContext, "sqlite3", "worker error callback", (err) => {
                _clearTimeout(timeout);
                reject(err);
            });
            const onExit = bindDefenseContextCallback(requireDefenseContext, "sqlite3", "worker exit callback", (code) => {
                _clearTimeout(timeout);
                if (code !== 0) {
                    resolve({
                        success: false,
                        error: `Worker exited with code ${code}`,
                    });
                }
            });
            const dispatchMessage = (result) => {
                try {
                    onMessage(result);
                }
                catch (error) {
                    _clearTimeout(timeout);
                    const message = error instanceof Error ? error.message : String(error);
                    resolve({
                        success: false,
                        error: sanitizeHostErrorMessage(message),
                    });
                }
            };
            const dispatchError = (err) => {
                try {
                    onError(err);
                }
                catch (error) {
                    _clearTimeout(timeout);
                    const message = error instanceof Error ? error.message : String(error);
                    reject(new Error(sanitizeHostErrorMessage(message)));
                }
            };
            const dispatchExit = (code) => {
                try {
                    onExit(code);
                }
                catch (error) {
                    _clearTimeout(timeout);
                    const message = error instanceof Error ? error.message : String(error);
                    resolve({
                        success: false,
                        error: sanitizeHostErrorMessage(message),
                    });
                }
            };
            worker.on("message", dispatchMessage);
            worker.on("error", dispatchError);
            worker.on("exit", dispatchExit);
        });
    }
    catch (e) {
        // Worker failed to load - do not fall back to direct execution
        // as it has no timeout protection (DoS risk)
        const message = sanitizeHostErrorMessage(e.message);
        throw new Error(`sqlite3 worker failed to load: ${message}`);
    }
}
export const sqlite3Command = {
    name: "sqlite3",
    async execute(args, ctx) {
        // Real sqlite3 accepts both -help and --help
        if (hasHelpFlag(args) || args.includes("-help"))
            return showHelp(sqlite3Help);
        const parsed = parseArgs(args);
        if ("exitCode" in parsed)
            return parsed;
        const { options, database, sql: sqlArg, showVersion } = parsed;
        // Handle -version
        if (showVersion) {
            const version = await getSqliteVersion();
            return {
                stdout: `${version}\n`,
                stderr: "",
                exitCode: 0,
            };
        }
        if (!database) {
            return {
                stdout: "",
                stderr: "sqlite3: missing database argument\n",
                exitCode: 1,
            };
        }
        // Get SQL from argument or stdin, prepend -cmd if provided
        let sql = sqlArg || ctx.stdin.trim();
        if (options.cmd) {
            sql = options.cmd + (sql ? `; ${sql}` : "");
        }
        if (!sql) {
            return {
                stdout: "",
                stderr: "sqlite3: no SQL provided\n",
                exitCode: 1,
            };
        }
        // Load database buffer
        const isMemory = database === ":memory:";
        let dbPath = "";
        let dbBuffer = null;
        try {
            if (!isMemory) {
                dbPath = ctx.fs.resolvePath(ctx.cwd, database);
                if (await ctx.fs.exists(dbPath)) {
                    dbBuffer = await ctx.fs.readFileBuffer(dbPath);
                }
            }
        }
        catch (e) {
            const message = sanitizeErrorMessage(e.message);
            return {
                stdout: "",
                stderr: `sqlite3: unable to open database "${database}": ${message}\n`,
                exitCode: 1,
            };
        }
        // Get timeout from execution limits or use default
        const timeoutMs = ctx.limits?.maxSqliteTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
        // Execute in worker with timeout
        const workerInput = {
            protocolToken: generateWorkerProtocolToken(),
            dbBuffer,
            sql,
            options: {
                bail: options.bail,
                echo: options.echo,
            },
        };
        let result;
        try {
            result = await executeInWorker(workerInput, timeoutMs, ctx.requireDefenseContext);
        }
        catch (e) {
            const message = sanitizeHostErrorMessage(e.message);
            return {
                stdout: "",
                stderr: `sqlite3: worker error: ${message}\n`,
                exitCode: 1,
            };
        }
        if (!result.success) {
            const message = sanitizeHostErrorMessage(result.error);
            return {
                stdout: "",
                stderr: `sqlite3: ${message}\n`,
                exitCode: 1,
            };
        }
        // Format output
        const formatOptions = {
            mode: options.mode,
            header: options.header,
            separator: options.separator,
            newline: options.newline,
            nullValue: options.nullValue,
        };
        let stdout = "";
        // Echo SQL if requested
        if (options.echo) {
            stdout += `${sql}\n`;
        }
        // Process results
        let hadError = false;
        for (const stmtResult of result.results) {
            if (stmtResult.type === "error") {
                if (options.bail) {
                    return {
                        stdout,
                        stderr: `Error: ${stmtResult.error}\n`,
                        exitCode: 1,
                    };
                }
                stdout += `Error: ${stmtResult.error}\n`;
                hadError = true;
            }
            else if (stmtResult.columns && stmtResult.rows) {
                if (stmtResult.rows.length > 0 || options.header) {
                    stdout += formatOutput(stmtResult.columns, stmtResult.rows, formatOptions);
                }
            }
        }
        // Write back modifications if needed
        if (result.hasModifications &&
            !options.readonly &&
            !isMemory &&
            dbPath &&
            result.dbBuffer) {
            try {
                await ctx.fs.writeFile(dbPath, result.dbBuffer);
            }
            catch (e) {
                const message = sanitizeErrorMessage(e.message);
                return {
                    stdout,
                    stderr: `sqlite3: failed to write database: ${message}\n`,
                    exitCode: 1,
                };
            }
        }
        return { stdout, stderr: "", exitCode: hadError && options.bail ? 1 : 0 };
    },
};
export const flagsForFuzzing = {
    name: "sqlite3",
    flags: [],
    needsArgs: true,
};
