/**
 * State Bridge — reads Antigravity's USS state from the SQLite database.
 *
 * Antigravity stores settings, conversation metadata, and agent preferences
 * in `state.vscdb` (SQLite). This bridge provides read-only access to that data.
 *
 * VERIFIED against live state.vscdb on 2026-02-28.
 *
 * @module transport/state-bridge
 */

import * as path from 'path';
import * as fs from 'fs';
import { IDisposable } from '../core/disposable';
import { StateReadError } from '../core/errors';
import { Logger } from '../core/logger';
import type {
    IAgentPreferences,
    TerminalExecutionPolicy,
    ArtifactReviewPolicy,
} from '../core/types';

const log = new Logger('StateBridge');

/**
 * USS (Unified State Sync) keys in state.vscdb.
 *
 * VERIFIED: All keys listed below were confirmed to exist
 * in a live Antigravity v1.107.0 installation on 2026-02-28.
 * Values are Base64-encoded protobuf unless noted otherwise.
 */
export const USSKeys = {
    /** Agent preferences — terminal policy, review policy, secure mode, etc. (1020 bytes) */
    AGENT_PREFERENCES: 'antigravityUnifiedStateSync.agentPreferences',

    /** Conversation/trajectory summaries — titles, timestamps, workspace URIs (74KB+) */
    TRAJECTORY_SUMMARIES: 'antigravityUnifiedStateSync.trajectorySummaries',

    /** Agent manager window state (192 bytes) */
    AGENT_MANAGER_WINDOW: 'antigravityUnifiedStateSync.agentManagerWindow',

    /** Enterprise override store (56 bytes) */
    OVERRIDE_STORE: 'antigravityUnifiedStateSync.overrideStore',

    /** Model preferences — selected model, sentinel key */
    MODEL_PREFERENCES: 'antigravityUnifiedStateSync.modelPreferences',

    /** Artifact review state (1204 bytes) */
    ARTIFACT_REVIEW: 'antigravityUnifiedStateSync.artifactReview',

    /** Browser preferences (380 bytes) */
    BROWSER_PREFERENCES: 'antigravityUnifiedStateSync.browserPreferences',

    /** Editor preferences (108 bytes) */
    EDITOR_PREFERENCES: 'antigravityUnifiedStateSync.editorPreferences',

    /** Tab preferences (404 bytes) */
    TAB_PREFERENCES: 'antigravityUnifiedStateSync.tabPreferences',

    /** Window preferences (44 bytes) */
    WINDOW_PREFERENCES: 'antigravityUnifiedStateSync.windowPreferences',

    /** Scratch/playground workspaces (268 bytes) */
    SCRATCH_WORKSPACES: 'antigravityUnifiedStateSync.scratchWorkspaces',

    /** Sidebar workspaces — recent workspace list (5604 bytes) */
    SIDEBAR_WORKSPACES: 'antigravityUnifiedStateSync.sidebarWorkspaces',

    /** User status info (5196 bytes) */
    USER_STATUS: 'antigravityUnifiedStateSync.userStatus',

    /** Model credits/usage info */
    MODEL_CREDITS: 'antigravityUnifiedStateSync.modelCredits',

    /** Onboarding state (140 bytes) */
    ONBOARDING: 'antigravityUnifiedStateSync.onboarding',

    /** Seen NUX (new user experience) IDs (76 bytes) */
    SEEN_NUX_IDS: 'antigravityUnifiedStateSync.seenNuxIds',

    // ⚠️ Jetski-specific state (separate sync namespace)
    /** Agent manager initialization state — contains auth tokens, workspace map (5144 bytes) */
    AGENT_MANAGER_INIT: 'jetskiStateSync.agentManagerInitState',

    // ⚠️ Non-USS but relevant keys
    /** All user settings — JSON format */
    ALL_USER_SETTINGS: 'antigravityUserSettings.allUserSettings',

    /** Allowed model configs for commands */
    ALLOWED_COMMAND_MODEL_CONFIGS: 'antigravity_allowed_command_model_configs',

    /** Chat session store index (JSON: {"version":1,"entries":{}}) */
    CHAT_SESSION_INDEX: 'chat.ChatSessionStore.index',
} as const;

/**
 * Keys that contain sensitive data and MUST NOT be exposed through the SDK.
 *
 * VERIFIED 2026-02-28:
 * - oauthToken: OAuth access token (732 bytes)
 * - agentManagerInitState: Contains LIVE ya29.* access token + g1//* refresh token!
 * - antigravityAuthStatus: Auth status
 */
const SENSITIVE_KEYS = new Set([
    'antigravityUnifiedStateSync.oauthToken',
    'jetskiStateSync.agentManagerInitState',
    'antigravityAuthStatus',
]);

/**
 * Protobuf sentinel keys found in agentPreferences.
 *
 * ALL 16 sentinel keys verified from live state.vscdb on 2026-02-28.
 * Each sentinel key string is followed by a small Base64 value encoding
 * a protobuf varint (the actual preference value).
 */
const SENTINEL_KEYS = {
    PLANNING_MODE: 'planningModeSentinelKey',
    ARTIFACT_REVIEW_POLICY: 'artifactReviewPolicySentinelKey',
    TERMINAL_AUTO_EXECUTION_POLICY: 'terminalAutoExecutionPolicySentinelKey',
    TERMINAL_ALLOWED_COMMANDS: 'terminalAllowedCommandsSentinelKey',
    TERMINAL_DENIED_COMMANDS: 'terminalDeniedCommandsSentinelKey',
    ALLOW_NON_WORKSPACE_FILES: 'allowAgentAccessNonWorkspaceFilesSentinelKey',
    ALLOW_GITIGNORE_ACCESS: 'allowCascadeAccessGitignoreFilesSentinelKey',
    SECURE_MODE: 'secureModeSentinelKey',
    EXPLAIN_FIX_IN_CONVO: 'explainAndFixInCurrentConversationSentinelKey',
    AUTO_CONTINUE_ON_MAX: 'autoContinueOnMaxGeneratorInvocationsSentinelKey',
    DISABLE_AUTO_OPEN_EDITED: 'disableAutoOpenEditedFilesSentinelKey',
    ENABLE_SOUNDS: 'enableSoundsForSpecialEventsSentinelKey',
    DISABLE_AUTO_FIX_LINTS: 'disableCascadeAutoFixLintsSentinelKey',
    ENABLE_SHELL_INTEGRATION: 'enableShellIntegrationSentinelKey',
    SANDBOX_ALLOW_NETWORK: 'sandboxAllowNetworkSentinelKey',
    ENABLE_TERMINAL_SANDBOX: 'enableTerminalSandboxSentinelKey',
} as const;

/**
 * Reads Antigravity's internal state from the SQLite database.
 *
 * Uses **sql.js** (pure JavaScript SQLite, compiled to WASM) which is
 * verified to work in Antigravity's Extension Host (unlike better-sqlite3
 * which fails due to ABI mismatch with Electron v22.21.1 / ABI v140).
 *
 * @example
 * ```typescript
 * const bridge = new StateBridge();
 * await bridge.initialize();
 *
 * const prefs = await bridge.getAgentPreferences();
 * console.log(prefs.terminalExecutionPolicy);
 * ```
 */
export class StateBridge implements IDisposable {
    private _dbPath: string | null = null;
    private _db: any = null; // sql.js Database instance
    private _disposed = false;

    /**
     * Initialize the state bridge by locating and opening state database.
     *
     * @throws {StateReadError} If the database cannot be found
     */
    async initialize(): Promise<void> {
        const dbPath = this._findStateDb();

        if (!dbPath) {
            throw new StateReadError('state.vscdb', 'Could not locate Antigravity state database');
        }

        this._dbPath = dbPath;

        // Open with sql.js (pure JS — verified working in Extension Host)
        try {
            const path = require('path');
            const fs = require('fs');

            // Try to load sql.js from multiple locations:
            // 1. Adjacent sql-wasm.js (for VSIX bundles where consumer copies it to dist/)
            // 2. Standard require('sql.js') (for npm install / dev setups)
            let initSqlJs: any;
            const localSqlJs = path.join(__dirname, 'sql-wasm.js');
            if (fs.existsSync(localSqlJs)) {
                initSqlJs = require(localSqlJs);
            } else {
                initSqlJs = require('sql.js');
            }

            // Auto-locate sql-wasm.wasm — try multiple paths so devs
            // don't need to manually copy anything after `npm install`
            const candidates = [
                // 1. Adjacent to this file (if wasm was bundled/copied to dist/)
                path.join(__dirname, 'sql-wasm.wasm'),
                // 2. sql.js package dist/ (standard npm install)
                path.resolve(__dirname, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
                // 3. Hoisted node_modules (monorepo / npm workspaces)
                path.resolve(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
                // 4. Walk up to find it (deep hoisting)
                path.resolve(__dirname, '..', '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
            ];

            // Try require.resolve — works in all layouts
            try {
                const sqlJsMain = require.resolve('sql.js');
                candidates.unshift(path.join(path.dirname(sqlJsMain), 'sql-wasm.wasm'));
            } catch {
                // sql.js might not have a resolvable main in all setups
            }

            let wasmPath: string | null = null;
            for (const p of candidates) {
                if (fs.existsSync(p)) {
                    wasmPath = p;
                    break;
                }
            }

            if (!wasmPath) {
                throw new Error('sql-wasm.wasm not found in any expected location');
            }

            const SQL = await initSqlJs({
                locateFile: () => wasmPath!,
            });
            const fileBuffer = fs.readFileSync(dbPath);
            this._db = new SQL.Database(fileBuffer);
            log.info(`State database opened via sql.js: ${dbPath}`);
        } catch (error) {
            log.warn('sql.js not available, will use child_process fallback', error);
        }
    }

    /**
     * Read a raw value from the state database.
     *
     * @param key - The SQLite key to read
     * @returns The raw string value, or null if not found
     * @throws {StateReadError} If the key is sensitive or read fails
     */
    async getRawValue(key: string): Promise<string | null> {
        if (this._disposed) {
            throw new StateReadError(key, 'StateBridge has been disposed');
        }

        if (!this._dbPath) {
            throw new StateReadError(key, 'StateBridge not initialized');
        }

        // Block access to sensitive keys
        if (SENSITIVE_KEYS.has(key)) {
            throw new StateReadError(key, 'Access to sensitive keys is blocked by the SDK for security');
        }

        try {
            if (this._db) {
                return this._querySqlJs(key);
            }
            return await this._queryChildProcess(key);
        } catch (error) {
            if (error instanceof StateReadError) throw error;
            const msg = error instanceof Error ? error.message : String(error);
            throw new StateReadError(key, msg);
        }
    }

    /**
     * Get agent preferences from USS.
     *
     * @returns Parsed agent preferences
     */
    async getAgentPreferences(): Promise<IAgentPreferences> {
        const raw = await this.getRawValue(USSKeys.AGENT_PREFERENCES);

        if (!raw) {
            log.warn('No agent preferences found, returning defaults');
            return this._defaultPreferences();
        }

        try {
            return this._parseAgentPreferences(raw);
        } catch (error) {
            log.error('Failed to parse preferences, returning defaults', error);
            return this._defaultPreferences();
        }
    }

    /**
     * Get all stored USS keys from the state database.
     *
     * @returns List of key names related to Antigravity (excludes sensitive keys)
     */
    async getAntigravityKeys(): Promise<string[]> {
        if (!this._dbPath) {
            throw new StateReadError('*', 'StateBridge not initialized');
        }

        let keys: string[];

        if (this._db) {
            const result = this._db.exec(
                "SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%' OR key LIKE 'chat.%'",
            );
            keys = result.length > 0 ? result[0].values.map((r: any[]) => r[0] as string) : [];
        } else {
            const result = await this._queryChildProcess('*');
            keys = result ? result.split('\n').map((l: string) => l.trim()).filter(Boolean) : [];
        }

        // Filter out sensitive keys
        return keys.filter((k) => !SENSITIVE_KEYS.has(k));
    }

    /**
     * Query using sql.js (in-process, pure JS).
     */
    private _querySqlJs(key: string): string | null {
        const stmt = this._db.prepare('SELECT value FROM ItemTable WHERE key = $key');
        stmt.bind({ $key: key });
        if (stmt.step()) {
            const row = stmt.getAsObject();
            stmt.free();
            return (row.value as string) ?? null;
        }
        stmt.free();
        return null;
    }

    /**
     * Query using child_process sqlite3 CLI (fallback).
     */
    private async _queryChildProcess(key: string): Promise<string | null> {
        const { exec } = require('child_process');
        const { promisify } = require('util');
        const execAsync = promisify(exec);
        const sql =
            key === '*'
                ? "SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%'"
                : `SELECT value FROM ItemTable WHERE key = '${key.replace(/'/g, "''")}'`;

        try {
            const { stdout } = await execAsync(`sqlite3 "${this._dbPath}" "${sql}"`, {
                encoding: 'utf8',
                timeout: 5000,
            });
            return stdout.trim() || null;
        } catch {
            return null;
        }
    }

    /**
     * Locate the state.vscdb file across platforms.
     */
    private _findStateDb(): string | null {
        const candidates: string[] = [];

        // Windows (VERIFIED: this is the correct path)
        const appData = process.env.APPDATA;
        if (appData) {
            candidates.push(path.join(appData, 'Antigravity', 'User', 'globalStorage', 'state.vscdb'));
        }

        // macOS
        const home = process.env.HOME;
        if (home) {
            candidates.push(
                path.join(
                    home,
                    'Library',
                    'Application Support',
                    'Antigravity',
                    'User',
                    'globalStorage',
                    'state.vscdb',
                ),
            );
        }

        // Linux
        if (home) {
            candidates.push(
                path.join(home, '.config', 'Antigravity', 'User', 'globalStorage', 'state.vscdb'),
            );
        }

        for (const candidate of candidates) {
            if (fs.existsSync(candidate)) {
                return candidate;
            }
        }

        return null;
    }

    /**
     * Parse agent preferences from Base64(Protobuf).
     *
     * The protobuf structure uses "sentinel keys" as string fields:
     * - `planningModeSentinelKey` → nested message with Base64(varint)
     * - `terminalAutoExecutionPolicySentinelKey` → nested message with Base64(varint)
     * - `artifactReviewPolicySentinelKey` → nested message with Base64(varint)
     *
     * Each sentinel value is itself a small Base64 string (e.g., "EAM=" = varint 3 = EAGER).
     */
    private _parseAgentPreferences(raw: string): IAgentPreferences {
        const buffer = Buffer.from(raw, 'base64');
        const text = buffer.toString('utf8');

        // Extract all sentinel values
        const terminalPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.TERMINAL_AUTO_EXECUTION_POLICY);
        const artifactPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.ARTIFACT_REVIEW_POLICY);
        const planningMode = this._extractSentinelValue(text, SENTINEL_KEYS.PLANNING_MODE);
        const secureMode = this._extractSentinelValue(text, SENTINEL_KEYS.SECURE_MODE);
        const terminalSandbox = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_TERMINAL_SANDBOX);
        const sandboxNetwork = this._extractSentinelValue(text, SENTINEL_KEYS.SANDBOX_ALLOW_NETWORK);
        const shellIntegration = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SHELL_INTEGRATION);
        const nonWorkspaceFiles = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_NON_WORKSPACE_FILES);
        const gitignoreAccess = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_GITIGNORE_ACCESS);
        const explainFix = this._extractSentinelValue(text, SENTINEL_KEYS.EXPLAIN_FIX_IN_CONVO);
        const autoContinue = this._extractSentinelValue(text, SENTINEL_KEYS.AUTO_CONTINUE_ON_MAX);
        const disableAutoOpen = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_OPEN_EDITED);
        const enableSounds = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SOUNDS);
        const disableAutoFix = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_FIX_LINTS);

        return {
            terminalExecutionPolicy: (terminalPolicy ?? 1) as TerminalExecutionPolicy,
            artifactReviewPolicy: (artifactPolicy ?? 1) as ArtifactReviewPolicy,
            planningMode: planningMode ?? 0,
            secureModeEnabled: (secureMode ?? 0) === 1,
            terminalSandboxEnabled: (terminalSandbox ?? 0) === 1,
            sandboxAllowNetwork: (sandboxNetwork ?? 0) === 1,
            shellIntegrationEnabled: (shellIntegration ?? 1) === 1,
            allowNonWorkspaceFiles: (nonWorkspaceFiles ?? 0) === 1,
            allowGitignoreAccess: (gitignoreAccess ?? 0) === 1,
            explainFixInCurrentConvo: (explainFix ?? 0) === 1,
            autoContinueOnMax: autoContinue ?? 0,
            disableAutoOpenEdited: (disableAutoOpen ?? 0) === 1,
            enableSounds: (enableSounds ?? 0) === 1,
            disableAutoFixLints: (disableAutoFix ?? 0) === 1,
            allowedCommands: [],
            deniedCommands: [],
        };
    }

    /**
     * Extract a varint value from a protobuf sentinel key.
     *
     * The structure is: sentinel_key_string followed by a small
     * Base64 value like "EAM=" (which decodes to a protobuf varint).
     *
     * Known mappings:
     * - "CAE=" → field 1, value 1 (OFF / ALWAYS)
     * - "EAI=" → field 2, value 2 (AUTO / TURBO)
     * - "EAM=" → field 2, value 3 (EAGER / AUTO)
     */
    private _extractSentinelValue(text: string, sentinelKey: string): number | null {
        const idx = text.indexOf(sentinelKey);
        if (idx === -1) return null;

        // After the sentinel key, look for a small Base64 fragment
        const after = text.substring(idx + sentinelKey.length, idx + sentinelKey.length + 30);

        // Match a Base64 chunk (typically 4-8 chars ending with =)
        const b64Match = after.match(/([A-Za-z0-9+/]{2,8}={0,2})/);
        if (!b64Match) return null;

        try {
            const decoded = Buffer.from(b64Match[1], 'base64');
            // Protobuf varint: last byte of the value
            // For simple single-byte varints, the value is in the lower 7 bits
            if (decoded.length >= 2) {
                // The first byte is (field_number << 3 | wire_type)
                // The second byte is the actual value
                return decoded[1];
            } else if (decoded.length === 1) {
                return decoded[0];
            }
        } catch {
            // Not valid base64
        }

        return null;
    }

    private _defaultPreferences(): IAgentPreferences {
        return {
            terminalExecutionPolicy: 1 as TerminalExecutionPolicy, // OFF
            artifactReviewPolicy: 1 as ArtifactReviewPolicy, // ALWAYS
            planningMode: 0,
            secureModeEnabled: false,
            terminalSandboxEnabled: false,
            sandboxAllowNetwork: false,
            shellIntegrationEnabled: true,
            allowNonWorkspaceFiles: false,
            allowGitignoreAccess: false,
            explainFixInCurrentConvo: false,
            autoContinueOnMax: 0,
            disableAutoOpenEdited: false,
            enableSounds: false,
            disableAutoFixLints: false,
            allowedCommands: [],
            deniedCommands: [],
        };
    }

    dispose(): void {
        this._disposed = true;

        if (this._db) {
            try {
                this._db.close();
            } catch {
                // Ignore close errors
            }
            this._db = null;
        }

        this._dbPath = null;
    }
}
