import { SQLITE_ACTOR_WASM_BASE64 } from "./wasm_binary";
const DEFAULT_PAGE_SIZE = 4096;
export class SqliteActor {
    kv;
    pageSize;
    wasm = null;
    dbPtr = 0;
    maxPageId = -1;
    pageCache = new Map();
    static async create(kv, options) {
        const db = new SqliteActor(kv, options?.pageSize || DEFAULT_PAGE_SIZE);
        await db.init(options);
        return db;
    }
    constructor(kv, pageSize = DEFAULT_PAGE_SIZE) {
        this.kv = kv;
        this.pageSize = pageSize;
    }
    async init(options) {
        // 1. Initialize SQLite3 WASM
        // Use relative import that works in both dev (with dist built) and distributed package
        const { default: initSqlite3 } = await import("./sqlite-actor.js");
        const config = {
            locateFile: (path) => path,
        };
        if (options?.wasmModule) {
            config.instantiateWasm = (imports, successCallback) => {
                const instance = new WebAssembly.Instance(options.wasmModule, imports);
                successCallback(instance, options.wasmModule);
                return instance.exports;
            };
        }
        else if (options?.wasmBinary) {
            config.wasmBinary = options.wasmBinary;
        }
        else {
            config.wasmBinary = base64ToUint8Array(SQLITE_ACTOR_WASM_BASE64);
        }
        this.wasm = await initSqlite3(config);
        // 2. Pre-load all pages into memory cache for synchronous VFS access
        const metaMaxPageId = await this.kv.get("meta:maxPageId");
        this.maxPageId = metaMaxPageId ? Number(new TextDecoder().decode(metaMaxPageId)) : -1;
        for (let i = 0; i <= this.maxPageId; i++) {
            const page = await this.kv.get(`page:${i}`);
            if (page) {
                this.pageCache.set(i, page);
            }
        }
        // 3. Setup the global VFS bridge
        globalThis._sqlite_actor_vfs = {
            read: (ptr, amt, offset) => this._vfsRead(offset, ptr, amt),
            write: (ptr, amt, offset) => this._vfsWrite(offset, ptr, amt),
            fileSize: () => this._vfsFileSize(),
        };
        // 4. Open the database
        const pDb = this.wasm._malloc(4);
        const zFilenameBytes = new TextEncoder().encode("main.db\0");
        const zFilename = this.wasm._malloc(zFilenameBytes.length);
        this.wasm.HEAPU8.set(zFilenameBytes, zFilename);
        const rc = this.wasm._sqlite3_open(zFilename, pDb);
        if (rc !== 0) {
            const msg = this.wasm.UTF8ToString(this.wasm._sqlite3_errmsg(this.wasm.getValue(pDb, "*")));
            throw new Error(`Failed to open database (rc=${rc}): ${msg}`);
        }
        this.dbPtr = this.wasm.getValue(pDb, "*");
        this.wasm._free(pDb);
        this.wasm._free(zFilename);
        this.execute(`PRAGMA page_size = ${this.pageSize};`);
        this.execute("PRAGMA journal_mode = OFF;");
        this.execute("PRAGMA synchronous = OFF;");
    }
    execute(sql) {
        // Before each operation, ensure our VFS bridge points to THIS instance
        globalThis._sqlite_actor_vfs = {
            read: (ptr, amt, offset) => this._vfsRead(offset, ptr, amt),
            write: (ptr, amt, offset) => this._vfsWrite(offset, ptr, amt),
            fileSize: () => this._vfsFileSize(),
        };
        const sqlBytes = new TextEncoder().encode(sql + "\0");
        const zSql = this.wasm._malloc(sqlBytes.length);
        this.wasm.HEAPU8.set(sqlBytes, zSql);
        const rc = this.wasm._sqlite3_exec(this.dbPtr, zSql, 0, 0, 0);
        if (rc !== 0) {
            const msg = this.wasm.UTF8ToString(this.wasm._sqlite3_errmsg(this.dbPtr));
            this.wasm._free(zSql);
            throw new Error(`SQLite error: ${msg}\nSQL: ${sql}`);
        }
        this.wasm._free(zSql);
    }
    query(sql) {
        // Before each operation, ensure our VFS bridge points to THIS instance
        globalThis._sqlite_actor_vfs = {
            read: (ptr, amt, offset) => this._vfsRead(offset, ptr, amt),
            write: (ptr, amt, offset) => this._vfsWrite(offset, ptr, amt),
            fileSize: () => this._vfsFileSize(),
        };
        const results = [];
        const pStmt = this.wasm._malloc(4);
        const sqlBytes = new TextEncoder().encode(sql + "\0");
        const zSql = this.wasm._malloc(sqlBytes.length);
        this.wasm.HEAPU8.set(sqlBytes, zSql);
        let rc = this.wasm._sqlite3_prepare_v2(this.dbPtr, zSql, -1, pStmt, 0);
        if (rc !== 0) {
            const msg = this.wasm.UTF8ToString(this.wasm._sqlite3_errmsg(this.dbPtr));
            this.wasm._free(zSql);
            this.wasm._free(pStmt);
            throw new Error(`SQLite prepare error: ${msg}\nSQL: ${sql}`);
        }
        const stmtPtr = this.wasm.getValue(pStmt, "*");
        this.wasm._free(pStmt);
        this.wasm._free(zSql);
        try {
            const colCount = this.wasm._sqlite3_column_count(stmtPtr);
            while ((rc = this.wasm._sqlite3_step(stmtPtr)) === 100) { // SQLITE_ROW
                const row = {};
                for (let i = 0; i < colCount; i++) {
                    const name = this.wasm.UTF8ToString(this.wasm._sqlite3_column_name(stmtPtr, i));
                    const type = this.wasm._sqlite3_column_type(stmtPtr, i);
                    if (type === 1) { // SQLITE_INTEGER
                        row[name] = this.wasm._sqlite3_column_int(stmtPtr, i);
                    }
                    else if (type === 2) { // SQLITE_FLOAT
                        row[name] = this.wasm._sqlite3_column_double(stmtPtr, i);
                    }
                    else if (type === 3) { // SQLITE_TEXT
                        row[name] = this.wasm.UTF8ToString(this.wasm._sqlite3_column_text(stmtPtr, i));
                    }
                    else if (type === 5) { // SQLITE_NULL
                        row[name] = null;
                    }
                }
                results.push(row);
            }
        }
        finally {
            this.wasm._sqlite3_finalize(stmtPtr);
        }
        return results;
    }
    _vfsFileSize() {
        return BigInt((this.maxPageId + 1) * this.pageSize);
    }
    _vfsRead(offset, ptr, amt) {
        let remaining = amt;
        let outPos = ptr;
        let off = Number(offset);
        while (remaining > 0) {
            const pageId = Math.floor(off / this.pageSize);
            const pageOffset = off % this.pageSize;
            const n = Math.min(remaining, this.pageSize - pageOffset);
            const stored = this.pageCache.get(pageId);
            if (stored) {
                this.wasm.HEAPU8.set(stored.subarray(pageOffset, pageOffset + n), outPos);
            }
            else {
                this.wasm.HEAPU8.fill(0, outPos, outPos + n);
            }
            off += n;
            outPos += n;
            remaining -= n;
        }
        return 0; // SQLITE_OK
    }
    _vfsWrite(offset, ptr, amt) {
        let remaining = amt;
        let inPos = ptr;
        let off = Number(offset);
        while (remaining > 0) {
            const pageId = Math.floor(off / this.pageSize);
            const pageOffset = off % this.pageSize;
            const n = Math.min(remaining, this.pageSize - pageOffset);
            let page;
            if (n === this.pageSize) {
                page = new Uint8Array(this.pageSize);
            }
            else {
                const existing = this.pageCache.get(pageId);
                page = existing ? new Uint8Array(existing) : new Uint8Array(this.pageSize);
            }
            page.set(this.wasm.HEAPU8.subarray(inPos, inPos + n), pageOffset);
            // Update cache
            this.pageCache.set(pageId, page);
            // Update KV in background (synchronous VFS requirement)
            // Durable Objects ensure this happens eventually or the object restarts.
            this.kv.put(`page:${pageId}`, page);
            if (pageId > this.maxPageId) {
                this.maxPageId = pageId;
                this.kv.put("meta:maxPageId", new TextEncoder().encode(this.maxPageId.toString()));
            }
            off += n;
            inPos += n;
            remaining -= n;
        }
        return 0; // SQLITE_OK
    }
    close() {
        if (this.dbPtr) {
            this.wasm._sqlite3_close(this.dbPtr);
            this.dbPtr = 0;
        }
    }
}
function base64ToUint8Array(base64) {
    if (typeof atob !== "undefined") {
        const binaryString = atob(base64);
        const bytes = new Uint8Array(binaryString.length);
        for (let i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes;
    }
    else {
        // @ts-ignore
        return globalThis.Buffer.from(base64, "base64");
    }
}
