import { join, dirname } from 'node:path'
import { mkdirSync, existsSync } from 'node:fs'
import { randomBytes, randomUUID } from 'node:crypto'
import { createRequire } from 'node:module'
import { hashPassword } from './password.js'
import { registerCleanup } from './shutdown.js'

const requireFromRoot = createRequire(join(process.cwd(), 'package.json'))
const { DatabaseSync } = requireFromRoot('node:sqlite')

const SCHEMA_VERSION = 6
const DB_BUSY_TIMEOUT_MS = 5000

let dbInstance = null
let testPath = null

const SCHEMA = {
  schema_version: 'CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)',
  users: `CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    identifier TEXT UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    role TEXT NOT NULL DEFAULT 'member',
    created_at TEXT NOT NULL
  )`,
  users_v2: `CREATE TABLE users_new (
    id TEXT PRIMARY KEY,
    identifier TEXT UNIQUE NOT NULL,
    password_hash TEXT,
    role TEXT NOT NULL DEFAULT 'member',
    created_at TEXT NOT NULL,
    auth_provider TEXT NOT NULL DEFAULT 'local',
    oidc_issuer TEXT,
    oidc_sub TEXT
  )`,
  users_oidc_index: `CREATE UNIQUE INDEX IF NOT EXISTS users_oidc_unique ON users(oidc_issuer, oidc_sub) WHERE oidc_issuer IS NOT NULL AND oidc_sub IS NOT NULL`,
  sessions: `CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    created_at TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    FOREIGN KEY(user_id) REFERENCES users(id)
  )`,
  pois: `CREATE TABLE IF NOT EXISTS pois (
    id TEXT PRIMARY KEY,
    lat REAL NOT NULL,
    lng REAL NOT NULL,
    label TEXT NOT NULL DEFAULT '',
    icon_type TEXT NOT NULL DEFAULT 'pin'
  )`,
  devices: `CREATE TABLE IF NOT EXISTS devices (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL DEFAULT '',
    device_type TEXT NOT NULL,
    vendor TEXT,
    lat REAL NOT NULL,
    lng REAL NOT NULL,
    stream_url TEXT NOT NULL DEFAULT '',
    source_type TEXT NOT NULL DEFAULT 'mjpeg',
    config TEXT
  )`,
  alpr_nodes: `CREATE TABLE IF NOT EXISTS alpr_nodes (
    osm_id INTEGER PRIMARY KEY,
    lat REAL NOT NULL,
    lng REAL NOT NULL,
    manufacturer TEXT,
    direction INTEGER,
    tags TEXT NOT NULL,
    fetched_at TEXT NOT NULL
  )`,
  alpr_nodes_index: 'CREATE INDEX IF NOT EXISTS idx_alpr_lat_lng ON alpr_nodes(lat, lng)',
  alpr_tiles: `CREATE TABLE IF NOT EXISTS alpr_tiles (
    tile_key TEXT PRIMARY KEY,
    fetched_at TEXT NOT NULL
  )`,
}

const getDbPath = () => {
  if (testPath) return testPath
  if (process.env.DB_PATH) return process.env.DB_PATH
  const dir = join(process.cwd(), 'data')
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  return join(dir, 'kestrelos.db')
}

export const getAvatarsDir = () => {
  const dir = join(dirname(getDbPath()), 'avatars')
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  return dir
}

const getSchemaVersion = async (get) => {
  try {
    const row = await get('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1')
    return row?.version || 0
  }
  catch {
    return 0
  }
}

const setSchemaVersion = (run, version) => run('INSERT OR REPLACE INTO schema_version (version) VALUES (?)', [version])

const migrateToV2 = async (run, all) => {
  const info = await all('PRAGMA table_info(users)')
  if (info.some(c => c.name === 'auth_provider')) return

  await run('BEGIN TRANSACTION')
  try {
    await run(SCHEMA.users_v2)
    await run('INSERT INTO users_new (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) SELECT id, identifier, password_hash, role, created_at, ?, ?, ? FROM users', ['local', null, null])
    await run('DROP TABLE users')
    await run('ALTER TABLE users_new RENAME TO users')
    await run(SCHEMA.users_oidc_index)
    await run('COMMIT')
  }
  catch (error) {
    await run('ROLLBACK').catch(() => {})
    throw error
  }
}

const migrateToV3 = async (run, all) => {
  const info = await all('PRAGMA table_info(users)')
  if (info.some(c => c.name === 'avatar_path')) return
  await run('ALTER TABLE users ADD COLUMN avatar_path TEXT')
}

const migrateToV4 = async (run, all) => {
  const info = await all('PRAGMA table_info(users)')
  if (info.some(c => c.name === 'cot_password_hash')) return
  await run('ALTER TABLE users ADD COLUMN cot_password_hash TEXT')
}

const migrateToV5 = async (run, all) => {
  const tables = await all('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'alpr_nodes\'')
  if (tables.length > 0) return
  await run(SCHEMA.alpr_nodes)
  await run(SCHEMA.alpr_nodes_index)
}

const migrateToV6 = async (run, all) => {
  const tables = await all('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'alpr_tiles\'')
  if (tables.length > 0) return
  await run(SCHEMA.alpr_tiles)
}

const runMigrations = async (run, all, get) => {
  const version = await getSchemaVersion(get)
  if (version >= SCHEMA_VERSION) return
  if (version < 2) {
    await migrateToV2(run, all)
    await setSchemaVersion(run, 2)
  }
  if (version < 3) {
    await migrateToV3(run, all)
    await setSchemaVersion(run, 3)
  }
  if (version < 4) {
    await migrateToV4(run, all)
    await setSchemaVersion(run, 4)
  }
  if (version < 5) {
    await migrateToV5(run, all)
    await setSchemaVersion(run, 5)
  }
  if (version < 6) {
    await migrateToV6(run, all)
    await setSchemaVersion(run, 6)
  }
}

const initDb = async (db, run, all, get) => {
  try {
    await run('PRAGMA journal_mode = WAL')
  }
  catch {
    // WAL not supported (e.g., network filesystem)
  }

  await run(SCHEMA.schema_version)
  await run(SCHEMA.users)
  await runMigrations(run, all, get)
  await run(SCHEMA.sessions)
  await run(SCHEMA.pois)
  await run(SCHEMA.devices)
  await run(SCHEMA.alpr_nodes)
  await run(SCHEMA.alpr_nodes_index)
  await run(SCHEMA.alpr_tiles)

  if (!testPath) {
    // Bootstrap admin user on first run
    const PASSWORD_CHARS = Object.freeze('abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789')
    const generateRandomPassword = () =>
      Array.from(randomBytes(14), b => PASSWORD_CHARS[b % PASSWORD_CHARS.length]).join('')

    const row = await get('SELECT COUNT(*) as n FROM users')
    if (row?.n === 0) {
      const email = process.env.BOOTSTRAP_EMAIL?.trim()
      const password = process.env.BOOTSTRAP_PASSWORD
      const identifier = (email && password) ? email : 'admin'
      const plainPassword = (email && password) ? password : generateRandomPassword()

      await run(
        'INSERT INTO users (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
        [randomUUID(), identifier, hashPassword(plainPassword), 'admin', new Date().toISOString(), 'local', null, null],
      )

      if (!email || !password) {
        console.log(`\n[KestrelOS] No bootstrap admin configured. Default admin created. Sign in at /login with:\n\n  Identifier: ${identifier}\n  Password:   ${plainPassword}\n\n  Set BOOTSTRAP_EMAIL and BOOTSTRAP_PASSWORD to use your own credentials on first run.\n`)
      }
    }
  }
}

export async function getDb() {
  if (dbInstance) return dbInstance

  const sqliteDb = (() => {
    try {
      return new DatabaseSync(getDbPath(), { timeout: DB_BUSY_TIMEOUT_MS })
    }
    catch (err) {
      console.error('[db] Failed to open database:', err.message)
      throw err
    }
  })()

  const run = (sql, params = []) =>
    new Promise((resolve, reject) => {
      const stmt = sqliteDb.prepare(sql)
      try {
        stmt.run(...params)
        resolve()
      }
      catch (error) {
        reject(error)
      }
      finally {
        try {
          stmt.finalize()
        }
        catch {
          // ignore finalize errors
        }
      }
    })

  const all = (sql, params = []) =>
    new Promise((resolve, reject) => {
      const stmt = sqliteDb.prepare(sql)
      try {
        const rows = stmt.all(...params)
        resolve(rows)
      }
      catch (error) {
        reject(error)
      }
      finally {
        try {
          stmt.finalize()
        }
        catch {
          // ignore finalize errors
        }
      }
    })

  const get = (sql, params = []) =>
    new Promise((resolve, reject) => {
      const stmt = sqliteDb.prepare(sql)
      try {
        const row = stmt.get(...params)
        resolve(row)
      }
      catch (error) {
        reject(error)
      }
      finally {
        try {
          stmt.finalize()
        }
        catch {
          // ignore finalize errors
        }
      }
    })

  const wrappedDb = {
    close(callback) {
      try {
        sqliteDb.close()
        if (typeof callback === 'function') callback(null)
      }
      catch (error) {
        if (typeof callback === 'function') callback(error)
        else throw error
      }
    },
  }

  try {
    await initDb(wrappedDb, run, all, get)
  }
  catch (error) {
    wrappedDb.close()
    console.error('[db] Database initialization failed:', error.message)
    throw error
  }

  dbInstance = { db: wrappedDb, run, all, get }

  registerCleanup(async () => {
    if (dbInstance) {
      try {
        dbInstance.db.close()
      }
      catch (error) {
        console.error('[db] Error closing database during shutdown:', error?.message)
      }
      dbInstance = null
    }
  })

  return dbInstance
}

/**
 * Health check for database connection.
 * @returns {Promise<{ healthy: boolean, error?: string }>} Health status
 */
export async function healthCheck() {
  try {
    const db = await getDb()
    await db.get('SELECT 1')
    return { healthy: true }
  }
  catch (error) {
    return {
      healthy: false,
      error: error?.message || String(error),
    }
  }
}

/**
 * Database connection model documentation:
 *
 * KestrelOS uses SQLite with WAL (Write-Ahead Logging) mode for concurrent access.
 * - Single connection instance shared across all requests (singleton pattern)
 * - WAL mode allows multiple readers and one writer concurrently
 * - Connection is initialized on first getDb() call and reused thereafter
 * - Busy timeout is set to 5000ms to handle concurrent access gracefully
 * - Transactions are supported via withTransaction() helper
 *
 * Concurrency considerations:
 * - SQLite with WAL handles concurrent reads efficiently
 * - Writes are serialized (one at a time)
 * - For high write loads, consider migrating to PostgreSQL
 * - Current model is suitable for moderate traffic (< 100 req/sec)
 *
 * Connection lifecycle:
 * - Created on first getDb() call
 * - Persists for application lifetime
 * - Closed during graceful shutdown
 * - Test path can be set via setDbPathForTest() for testing
 */

/**
 * Execute a callback within a database transaction.
 * Automatically commits on success or rolls back on error.
 * @param {object} db - Database instance from getDb()
 * @param {Function} callback - Async function receiving { run, all, get }
 * @returns {Promise<any>} Result of callback
 */
export async function withTransaction(db, callback) {
  const { run } = db
  await run('BEGIN TRANSACTION')
  try {
    const result = await callback(db)
    await run('COMMIT')
    return result
  }
  catch (error) {
    await run('ROLLBACK').catch(() => {
      // Ignore rollback errors
    })
    throw error
  }
}

export function closeDb() {
  if (!dbInstance) return
  try {
    dbInstance.db.close()
  }
  catch (error) {
    console.error('[db] Error closing database:', error.message)
  }
  dbInstance = null
}

export function setDbPathForTest(path) {
  testPath = path || null
  closeDb()
}
