import sqlite3InitModule from '@sqlite.org/sqlite-wasm'
import { REQUIRED_TABLES, checkCompatibility, getMigrationScripts } from './releases.js'
import { SCHEMA_SQL, SCHEMA_VERSION } from './schema.js'

let db = null
let sqlite3 = null

async function initDatabase(config) {
  const { dbName, wasmBaseUrl } = config

  try {
    sqlite3 = await sqlite3InitModule({
      locateFile: (file) => new URL(file, wasmBaseUrl).href,
    })
  } catch (e) {
    // This is the only step that touches the network (downloading
    // sqlite3.wasm on a not-yet-cached first load), so tag the failure as a
    // connectivity problem — the UI offers a retry instead of the generic
    // "browser not compatible" message.
    throw new Error(`DB_LOAD_FAILED: ${e?.message || e}`, { cause: e })
  }

  const poolUtil = await sqlite3.installOpfsSAHPoolVfs({
    directory: `.${dbName}`,
    initialCapacity: 6,
  })
  db = new poolUtil.OpfsSAHPoolDb(`/${dbName}.db`)

  db.exec('PRAGMA foreign_keys = ON')

  return { vfs: 'opfs-sahpool', persistent: true }
}

function _readDbVersion() {
  const tableRows = db.exec(
    "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'",
    { returnValue: 'resultRows', rowMode: 'array' },
  )
  if (tableRows.length === 0) return null

  const versionRows = db.exec('SELECT MAX(version) AS version FROM schema_version', {
    returnValue: 'resultRows',
    rowMode: 'object',
  })
  return versionRows.length > 0 ? versionRows[0].version : null
}

function handleGetSchemaStatus() {
  const dbVersion = _readDbVersion()

  if (dbVersion === null) {
    // Fresh DB: create the schema and stamp the current version
    db.exec(SCHEMA_SQL)
    db.exec('INSERT INTO schema_version (version) VALUES (?)', { bind: [SCHEMA_VERSION] })
    return { status: 'fresh', version: SCHEMA_VERSION }
  }

  if (dbVersion === SCHEMA_VERSION) {
    return { status: 'up-to-date', version: dbVersion }
  }

  if (dbVersion > SCHEMA_VERSION) {
    return { status: 'incompatible', dbVersion, appVersion: SCHEMA_VERSION }
  }

  // Older DB — the app must obtain a user backup before calling runMigrations
  return { status: 'migration-pending', from: dbVersion, to: SCHEMA_VERSION }
}

function handleRunMigrations() {
  const from = _readDbVersion()

  if (from === null || from >= SCHEMA_VERSION) {
    return { status: 'up-to-date', version: from }
  }

  let migrations
  try {
    migrations = getMigrationScripts(from, SCHEMA_VERSION)
  } catch (e) {
    return { status: 'migration-failed', from, to: SCHEMA_VERSION, message: e.message }
  }

  for (const { schemaVersion, script } of migrations) {
    try {
      db.exec('BEGIN')
      if (script) db.exec(script)
      db.exec('INSERT INTO schema_version (version) VALUES (?)', { bind: [schemaVersion] })
      db.exec('COMMIT')
    } catch (e) {
      try {
        db.exec('ROLLBACK')
      } catch {
        // no open transaction left to roll back
      }
      return {
        status: 'migration-failed',
        from,
        to: SCHEMA_VERSION,
        failedAt: schemaVersion,
        message: e.message,
      }
    }
  }

  return { status: 'migrated', from, to: SCHEMA_VERSION }
}

function handleBatch(statements) {
  db.exec('BEGIN IMMEDIATE')
  try {
    for (const { sql, params } of statements) {
      db.exec(sql, { bind: params || undefined })
    }
    db.exec('COMMIT')
    return true
  } catch (e) {
    try {
      db.exec('ROLLBACK')
    } catch (_e) {
      // Ignore — the error we care about is `e`
    }
    throw e
  }
}

function handleExec(sql, params) {
  db.exec(sql, { bind: params || undefined })
  return true
}

function handleRun(sql, params) {
  db.exec(sql, { bind: params || undefined })
  return { changes: sqlite3.capi.sqlite3_changes(db.pointer) }
}

function handleSelectAll(sql, params) {
  return db.exec(sql, {
    bind: params || undefined,
    rowMode: 'object',
    returnValue: 'resultRows',
  })
}

function handleSelectOne(sql, params) {
  const rows = db.exec(sql, {
    bind: params || undefined,
    rowMode: 'object',
    returnValue: 'resultRows',
  })
  return rows.length > 0 ? rows[0] : null
}

function handleImportDb(bytes) {
  const uint8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)

  // 1. Try loading the file into an in-memory DB via sqlite3_deserialize
  let tempDb
  try {
    tempDb = new sqlite3.oo1.DB(':memory:')
    const n = uint8.byteLength
    const pBuf = sqlite3.wasm.alloc(n)
    sqlite3.wasm.heap8u().set(uint8, pBuf)
    const rc = sqlite3.capi.sqlite3_deserialize(
      tempDb.pointer,
      'main',
      pBuf,
      n,
      n,
      sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE | sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE,
    )
    if (rc !== 0) {
      const msg = sqlite3.capi.sqlite3_errmsg(tempDb.pointer)
      tempDb.close()
      return { success: false, error: 'INVALID_FILE', message: msg || `deserialize rc=${rc}` }
    }
    // Verify the DB can be read
    tempDb.exec('SELECT count(*) FROM sqlite_master')
  } catch (e) {
    if (tempDb) tempDb.close()
    return { success: false, error: 'INVALID_FILE', message: e.message }
  }

  // 2. Check required tables exist in the imported DB
  const importedTableRows = tempDb.exec(
    "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
    { returnValue: 'resultRows', rowMode: 'array' },
  )
  const importedTableNames = importedTableRows.map((r) => r[0])

  for (const required of REQUIRED_TABLES) {
    if (!importedTableNames.includes(required)) {
      tempDb.close()
      return {
        success: false,
        error: 'INCOMPLETE_DB',
        message: `Missing required table: ${required}`,
      }
    }
  }

  // 3. Read schema versions from both DBs
  const importedVersionRows = tempDb.exec(
    'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1',
    { returnValue: 'resultRows', rowMode: 'array' },
  )
  const importedVersion = importedVersionRows.length > 0 ? importedVersionRows[0][0] : null

  if (importedVersion === null) {
    tempDb.close()
    return {
      success: false,
      error: 'INCOMPLETE_DB',
      message: 'No schema version found in imported DB',
    }
  }

  const currentVersionRows = db.exec(
    'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1',
    { returnValue: 'resultRows', rowMode: 'array' },
  )
  const currentVersion = currentVersionRows.length > 0 ? currentVersionRows[0][0] : null

  // 4. Check compatibility
  const compat = checkCompatibility(importedVersion, currentVersion)

  if (!compat.compatible) {
    tempDb.close()
    return {
      success: false,
      error: 'INCOMPATIBLE_VERSION',
      message: `Schema version ${importedVersion} is incompatible with current version ${currentVersion}`,
      importedVersion,
      currentVersion,
    }
  }

  // 5. Run adapt scripts if needed
  if (compat.needsAdapt) {
    try {
      for (const script of compat.scripts) {
        tempDb.exec(script)
      }
    } catch (e) {
      tempDb.close()
      return { success: false, error: 'ADAPT_FAILED', message: e.message }
    }
  }

  // 6. Get current DB tables
  const currentTableRows = db.exec(
    "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
    { returnValue: 'resultRows', rowMode: 'array' },
  )
  const currentTableNames = currentTableRows.map((r) => r[0])

  // Only restore tables that exist in both databases.
  // schema_version is deliberately excluded: after the adapt scripts ran on the
  // temp DB (step 5), its data matches the *current* schema, so the live DB's
  // own version stamp is already correct. Copying the backup's (older) stamp
  // would make the next launch re-run migrations against an already-adapted
  // schema (see src/db/releases.js).
  const tablesToRestore = importedTableNames.filter(
    (t) => currentTableNames.includes(t) && t !== 'schema_version',
  )

  // 7. Table-by-table copy inside a single transaction so a mid-restore
  //    failure cannot leave the live DB partially wiped.
  const report = {
    success: true,
    tablesRestored: [],
    importedVersion,
    currentVersion,
    adapted: compat.needsAdapt,
  }

  db.exec('PRAGMA foreign_keys = OFF')

  let restoreError = null
  try {
    db.exec('BEGIN IMMEDIATE')
    for (const table of tablesToRestore) {
      db.exec(`DELETE FROM "${table}"`)

      const rows = tempDb.exec(`SELECT * FROM "${table}"`, {
        returnValue: 'resultRows',
        rowMode: 'object',
      })

      if (rows.length > 0) {
        const cols = Object.keys(rows[0])
        const currentColRows = db.exec(`PRAGMA table_info("${table}")`, {
          returnValue: 'resultRows',
          rowMode: 'object',
        })
        const currentCols = currentColRows.map((r) => r.name)
        const colsToInsert = cols.filter((c) => currentCols.includes(c))

        if (colsToInsert.length > 0) {
          const quotedCols = colsToInsert.map((c) => `"${c}"`).join(', ')
          const placeholders = colsToInsert.map(() => '?').join(', ')
          const insertSql = `INSERT INTO "${table}" (${quotedCols}) VALUES (${placeholders})`
          const stmt = db.prepare(insertSql)
          try {
            for (const row of rows) {
              stmt.bind(colsToInsert.map((c) => row[c]))
              stmt.stepReset()
            }
          } finally {
            stmt.finalize()
          }
        }
      }

      report.tablesRestored.push({ table, rows: rows.length })
    }
    db.exec('COMMIT')
  } catch (e) {
    try {
      db.exec('ROLLBACK')
    } catch (_e) {
      // Ignore — the error we care about is `e`
    }
    restoreError = e
  } finally {
    db.exec('PRAGMA foreign_keys = ON')
    tempDb.close()
  }

  if (restoreError) {
    return { success: false, error: 'RESTORE_FAILED', message: restoreError.message }
  }
  return report
}

function handleResetDb() {
  const tables = [
    'execution_log_item',
    'execution_log',
    'collection_session',
    'session_item',
    'combo_exercise',
    'suffix',
    'collection',
    'session',
    'combo',
    'exercise',
    'settings',
    'schema_version',
  ]

  db.exec('PRAGMA foreign_keys = OFF')
  try {
    for (const table of tables) {
      db.exec(`DROP TABLE IF EXISTS "${table}"`)
    }
    db.exec(SCHEMA_SQL)
    db.exec('INSERT INTO schema_version (version) VALUES (?)', { bind: [SCHEMA_VERSION] })
  } finally {
    db.exec('PRAGMA foreign_keys = ON')
  }
  return { success: true }
}

self.onmessage = async (e) => {
  const { id, type, ...args } = e.data
  try {
    let result
    switch (type) {
      case 'init':
        result = await initDatabase(args)
        break
      case 'getSchemaStatus':
        result = handleGetSchemaStatus()
        break
      case 'runMigrations':
        result = handleRunMigrations()
        break
      case 'batch':
        result = handleBatch(args.statements)
        break
      case 'exec':
        result = handleExec(args.sql, args.params)
        break
      case 'run':
        result = handleRun(args.sql, args.params)
        break
      case 'selectAll':
        result = handleSelectAll(args.sql, args.params)
        break
      case 'selectOne':
        result = handleSelectOne(args.sql, args.params)
        break
      case 'exportDb':
        result = sqlite3.capi.sqlite3_js_db_export(db)
        break
      case 'importDb':
        result = handleImportDb(args.bytes)
        break
      case 'resetDb':
        result = handleResetDb()
        break
      default:
        throw new Error(`Unknown message type: ${type}`)
    }
    self.postMessage({ id, result })
  } catch (err) {
    self.postMessage({ id, error: err.message })
  }
}
