class Database {
  constructor() {
    this._worker = null
    this._messageId = 0
    this._pending = new Map()
    this._initPromise = null
    this._broken = false
    this.info = null
    // Set when the DB is older than the app: { from, to }. The app must show
    // the blocking migration screen and call runPendingMigrations().
    this.migrationPending = null
  }

  async init() {
    if (this._initPromise) return this._initPromise
    this._initPromise = this._doInit().catch((err) => {
      // If the worker never became usable (e.g. the sqlite3.wasm download
      // failed on a not-yet-cached first load), tear it down so a later
      // init() call retries from scratch. When init failed *after* the DB
      // opened (e.g. DB_NEWER_THAN_APP) the worker stays alive: the app may
      // still need it (export, diagnostics) and a retry would not help.
      if (!this.info) {
        if (this._worker) {
          this._worker.terminate()
          this._worker = null
        }
        this._pending.clear()
        this._broken = false
        this._initPromise = null
      }
      throw err
    })
    return this._initPromise
  }

  async _doInit() {
    this._worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
    this._worker.onmessage = (e) => this._handleMessage(e)
    this._worker.onerror = (e) => this._handleError(e)

    // Initialize SQLite + OPFS
    // Compute the absolute base URL at runtime so the worker can locate
    // sqlite3.wasm correctly regardless of the deployment subdirectory.
    const wasmBaseUrl = new URL(import.meta.env.BASE_URL, window.location.href).href
    this.info = await this._send('init', {
      dbName: __APP_NAME__,
      wasmBaseUrl,
    })

    // Detect the schema state (creates the schema on a fresh DB)
    const status = await this._send('getSchemaStatus')

    if (status.status === 'incompatible') {
      throw new Error('DB_NEWER_THAN_APP')
    }

    if (status.status === 'migration-pending') {
      // Pause: the UI must obtain a user backup, then call runPendingMigrations().
      // The DB is open (exportDatabase works) but defaults are not initialized yet.
      this.migrationPending = { from: status.from, to: status.to }
      return this.info
    }

    // Initialize default settings
    await this._initDefaults()

    return this.info
  }

  async runPendingMigrations() {
    const result = await this._send('runMigrations')
    if (result.status === 'migration-failed') {
      console.error('DB migration failed:', result)
      throw new Error('DB_MIGRATION_FAILED')
    }
    this.migrationPending = null
    await this._initDefaults()
    return result
  }

  async _initDefaults() {
    // Check if user_uuid exists (first launch detection)
    const existing = await this._send('selectOne', {
      sql: "SELECT value FROM settings WHERE key = 'user_uuid'",
    })

    if (!existing) {
      const uuid = crypto.randomUUID()
      const defaults = [
        ['user_uuid', uuid],
        ['user_name', 'User'],
        ['language', 'en'],
        ['theme', 'light'],
        ['db_created_at', new Date().toISOString()],
      ]
      for (const [key, value] of defaults) {
        await this._send('exec', {
          sql: 'INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)',
          params: [key, value],
        })
      }
    }

    // Always update app_version to current
    await this._send('exec', {
      sql: 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
      params: ['app_version', __APP_VERSION__],
    })
  }

  _handleMessage(e) {
    const { id, result, error } = e.data
    const resolver = this._pending.get(id)
    if (resolver) {
      this._pending.delete(id)
      if (error) resolver.reject(new Error(error))
      else resolver.resolve(result)
    }
  }

  _handleError(e) {
    console.error('DB Worker error:', e)
    this._broken = true
    for (const [, { reject }] of this._pending) {
      reject(new Error('Worker crashed'))
    }
    this._pending.clear()
    // During startup the failure surfaces through init()'s rejection (which
    // offers a retry); the connection-lost overlay is only for a worker that
    // dies after a successful init.
    if (this.info) {
      window.dispatchEvent(new CustomEvent('db-connection-lost'))
    }
  }

  _send(type, args = {}) {
    if (this._broken) {
      return Promise.reject(new Error('DB connection lost'))
    }
    return new Promise((resolve, reject) => {
      const id = ++this._messageId
      this._pending.set(id, { resolve, reject })
      this._worker.postMessage({ id, type, ...args })
    })
  }

  async batch(statements) {
    return this._send('batch', { statements })
  }

  async exec(sql, params) {
    return this._send('exec', { sql, params })
  }

  async run(sql, params) {
    return this._send('run', { sql, params })
  }

  async selectAll(sql, params) {
    return this._send('selectAll', { sql, params })
  }

  async selectOne(sql, params) {
    return this._send('selectOne', { sql, params })
  }

  async exportDatabase() {
    return this._send('exportDb')
  }

  async importDatabase(bytes) {
    return this._send('importDb', { bytes })
  }

  async resetDatabase() {
    await this._send('resetDb')
    await this._initDefaults()
  }
}

export const db = new Database()
