// src/builtins/sqlite-storage/sqlite-storage.provider.ts
import Database from "better-sqlite3";
import * as fs from "fs";
import * as path from "path";
import { randomUUID } from "crypto";
var LOCATION_TYPES = {
  // New location names (from StorageLocationManager)
  data: "structured",
  // settings, events, trails — SQL only
  media: "files",
  // crops, snapshots, thumbnails — files only
  recordings: "files",
  // video segments — files only
  models: "files",
  // ONNX/TFLite models — files only
  cache: "files",
  // temp files — files only
  logs: "files",
  // Winston log files — files only
  // Legacy location names (backward compat)
  config: "structured",
  events: "structured",
  addon: "both"
};
var SqliteStructuredStorage = class {
  constructor(db) {
    this.db = db;
  }
  ensuredTables = /* @__PURE__ */ new Set();
  ensureTable(collection) {
    if (this.ensuredTables.has(collection)) return;
    this.db.exec(
      `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT)`
    );
    this.ensuredTables.add(collection);
  }
  async insert(record) {
    this.ensureTable(record.collection);
    const id = record.id || randomUUID();
    const newRecord = {
      collection: record.collection,
      id,
      data: record.data
    };
    this.db.prepare(`INSERT INTO "${record.collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(newRecord.data));
    return newRecord;
  }
  async query(collection, filter) {
    this.ensureTable(collection);
    const { sql, params } = this.buildSelect(collection, filter);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map((row) => ({
      collection,
      id: row.id,
      data: JSON.parse(row.data)
    }));
  }
  async update(collection, id, data) {
    this.ensureTable(collection);
    this.db.prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
    return { collection, id, data };
  }
  async delete(collection, id) {
    this.ensureTable(collection);
    this.db.prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(id);
  }
  async count(collection, filter) {
    this.ensureTable(collection);
    const { sql, params } = this.buildCount(collection, filter);
    const row = this.db.prepare(sql).get(...params);
    return row.cnt;
  }
  buildWhereClause(filter) {
    if (!filter) return { clause: "", params: [] };
    const conditions = [];
    const params = [];
    if (filter.where) {
      for (const [field, value] of Object.entries(filter.where)) {
        if (field === "id") {
          conditions.push("id = ?");
          params.push(value);
        } else {
          conditions.push(`json_extract(data, '$.${field}') = ?`);
          params.push(value);
        }
      }
    }
    if (filter.whereIn) {
      for (const [field, values] of Object.entries(filter.whereIn)) {
        const placeholders = values.map(() => "?").join(", ");
        if (field === "id") {
          conditions.push(`id IN (${placeholders})`);
        } else {
          conditions.push(`json_extract(data, '$.${field}') IN (${placeholders})`);
        }
        params.push(...values);
      }
    }
    if (filter.whereBetween) {
      for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
        if (field === "id") {
          conditions.push("id BETWEEN ? AND ?");
        } else {
          conditions.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`);
        }
        params.push(low, high);
      }
    }
    const clause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
    return { clause, params };
  }
  buildSelect(collection, filter) {
    const { clause, params } = this.buildWhereClause(filter);
    let sql = `SELECT id, data FROM "${collection}"${clause}`;
    if (filter?.orderBy) {
      const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
      if (filter.orderBy.field === "id") {
        sql += ` ORDER BY id ${dir}`;
      } else {
        sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`;
      }
    }
    if (filter?.limit !== void 0) {
      sql += ` LIMIT ?`;
      params.push(filter.limit);
    }
    if (filter?.offset !== void 0) {
      sql += ` OFFSET ?`;
      params.push(filter.offset);
    }
    return { sql, params };
  }
  buildCount(collection, filter) {
    const { clause, params } = this.buildWhereClause(filter);
    return { sql: `SELECT COUNT(*) as cnt FROM "${collection}"${clause}`, params };
  }
};
var FileSystemStorage = class {
  constructor(basePath) {
    this.basePath = basePath;
  }
  async readFile(filePath) {
    const fullPath = path.join(this.basePath, filePath);
    return fs.promises.readFile(fullPath);
  }
  async writeFile(filePath, data) {
    const fullPath = path.join(this.basePath, filePath);
    fs.mkdirSync(path.dirname(fullPath), { recursive: true });
    await fs.promises.writeFile(fullPath, data);
  }
  async deleteFile(filePath) {
    const fullPath = path.join(this.basePath, filePath);
    await fs.promises.unlink(fullPath);
  }
  async listFiles(prefix) {
    const searchDir = prefix ? path.join(this.basePath, prefix) : this.basePath;
    try {
      const entries = await fs.promises.readdir(searchDir, { recursive: true });
      const files = [];
      for (const entry of entries) {
        const entryStr = String(entry);
        const relative = prefix ? path.join(prefix, entryStr) : entryStr;
        const fullPath = path.join(this.basePath, relative);
        try {
          const stat = await fs.promises.stat(fullPath);
          if (stat.isFile()) {
            files.push(relative);
          }
        } catch {
        }
      }
      return files;
    } catch {
      return [];
    }
  }
  async getFileUrl(_path) {
    return null;
  }
  async exists(filePath) {
    const fullPath = path.join(this.basePath, filePath);
    try {
      await fs.promises.access(fullPath, fs.constants.F_OK);
      return true;
    } catch {
      return false;
    }
  }
};
var SqliteStorageProvider = class {
  mainDb = null;
  sharedStructured = null;
  locations = /* @__PURE__ */ new Map();
  async initialize() {
  }
  /**
   * Configure all storage locations.
   * ONE single SQLite database (camstack.db) is used for ALL structured storage.
   * File-based locations use the filesystem at their configured path.
   */
  async configure(config) {
    const dataPath = config.locations["data"] ?? config.locations["config"] ?? Object.values(config.locations)[0];
    if (!dataPath) throw new Error("No data path configured for SQLite storage");
    fs.mkdirSync(dataPath, { recursive: true });
    const dbPath = path.join(dataPath, "camstack.db");
    this.mainDb = new Database(dbPath);
    this.mainDb.pragma("journal_mode = WAL");
    this.sharedStructured = new SqliteStructuredStorage(this.mainDb);
    for (const [name, dirPath] of Object.entries(config.locations)) {
      const locationName = name;
      const locationType = LOCATION_TYPES[name] ?? "files";
      fs.mkdirSync(dirPath, { recursive: true });
      const location = {};
      if (locationType === "structured" || locationType === "both") {
        location.structured = this.sharedStructured;
      }
      if (locationType === "files" || locationType === "both") {
        location.files = new FileSystemStorage(dirPath);
      }
      this.locations.set(locationName, location);
    }
  }
  getLocation(name) {
    const location = this.locations.get(name);
    if (!location) {
      throw new Error(`Storage location "${name}" not found`);
    }
    return location;
  }
  async shutdown() {
    if (this.mainDb) {
      this.mainDb.close();
      this.mainDb = null;
      this.sharedStructured = null;
    }
    this.locations.clear();
  }
  async export(_locationName) {
    throw new Error("Export not yet implemented");
  }
  async import(_locationName, _data) {
    throw new Error("Import not yet implemented");
  }
};

// src/builtins/sqlite-storage/sqlite-storage.addon.ts
var SqliteStorageAddon = class {
  manifest = {
    id: "sqlite-storage",
    name: "SQLite Storage",
    version: "1.0.0",
    capabilities: ["storage"]
  };
  provider = null;
  async initialize(context) {
    const storageConfig = {
      locations: { ...context.locationPaths }
    };
    this.provider = new SqliteStorageProvider();
    await this.provider.configure(storageConfig);
    context.logger.info("SQLite storage initialized");
  }
  async shutdown() {
    await this.provider?.shutdown();
  }
  getProvider() {
    if (!this.provider) throw new Error("SQLite storage not initialized");
    return this.provider;
  }
  getCapabilityProvider(name) {
    if (name === "storage" && this.provider) {
      return this.provider;
    }
    return null;
  }
  getConfigSchema() {
    return {
      sections: []
    };
  }
  getConfig() {
    return {};
  }
  async onConfigChange(_config) {
  }
};

// src/builtins/sqlite-storage/settings-store.ts
import Database2 from "better-sqlite3";

// src/builtins/sqlite-storage/sql-schema.ts
var CORE_TABLE_DDL = [
  // Settings tables
  `CREATE TABLE IF NOT EXISTS system_settings (
    key TEXT PRIMARY KEY,
    value JSON NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (unixepoch())
  )`,
  `CREATE TABLE IF NOT EXISTS addon_settings (
    addon_id TEXT NOT NULL,
    key TEXT NOT NULL,
    value JSON NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
    PRIMARY KEY (addon_id, key)
  )`,
  `CREATE TABLE IF NOT EXISTS provider_settings (
    provider_id TEXT NOT NULL,
    key TEXT NOT NULL,
    value JSON NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
    PRIMARY KEY (provider_id, key)
  )`,
  `CREATE TABLE IF NOT EXISTS device_settings (
    device_id TEXT NOT NULL,
    key TEXT NOT NULL,
    value JSON NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
    PRIMARY KEY (device_id, key)
  )`,
  // Detection events
  `CREATE TABLE IF NOT EXISTS detection_events (
    id TEXT PRIMARY KEY,
    timestamp INTEGER NOT NULL,
    device_id TEXT NOT NULL,
    class_name TEXT NOT NULL,
    score REAL NOT NULL,
    severity TEXT NOT NULL,
    track_id TEXT,
    zones JSON,
    recognition JSON,
    media_files JSON,
    data JSON
  )`,
  `CREATE INDEX IF NOT EXISTS idx_det_device_ts ON detection_events(device_id, timestamp)`,
  `CREATE INDEX IF NOT EXISTS idx_det_class_ts ON detection_events(class_name, timestamp)`,
  // Audio levels
  `CREATE TABLE IF NOT EXISTS audio_levels (
    id TEXT PRIMARY KEY,
    timestamp INTEGER NOT NULL,
    device_id TEXT NOT NULL,
    dbfs REAL NOT NULL,
    rms REAL NOT NULL,
    state TEXT NOT NULL
  )`,
  `CREATE INDEX IF NOT EXISTS idx_audio_device_ts ON audio_levels(device_id, timestamp)`,
  // Track trails
  `CREATE TABLE IF NOT EXISTS track_trails (
    track_id TEXT PRIMARY KEY,
    device_id TEXT NOT NULL,
    class_name TEXT NOT NULL,
    first_seen INTEGER NOT NULL,
    last_seen INTEGER NOT NULL,
    positions JSON NOT NULL,
    snapshots JSON,
    total_distance REAL,
    zones_visited JSON
  )`,
  `CREATE INDEX IF NOT EXISTS idx_trails_device_ts ON track_trails(device_id, first_seen)`
];
function addonTableToDdl(schema) {
  const pks = schema.columns.filter((c) => c.primaryKey).map((c) => c.name);
  const colDefs = schema.columns.map((c) => {
    const parts = [c.name, c.type];
    if (c.notNull) parts.push("NOT NULL");
    return parts.join(" ");
  });
  let ddl = `CREATE TABLE IF NOT EXISTS ${schema.name} (
  ${colDefs.join(",\n  ")}`;
  if (pks.length > 0) {
    ddl += `,
  PRIMARY KEY (${pks.join(", ")})`;
  }
  ddl += "\n)";
  const stmts = [ddl];
  for (const idx of schema.indexes ?? []) {
    const unique = idx.unique ? "UNIQUE " : "";
    stmts.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name}(${idx.columns.join(", ")})`);
  }
  return stmts;
}

// src/builtins/sqlite-storage/settings-store.ts
import { RUNTIME_DEFAULTS } from "@camstack/kernel";
var SettingsStore = class {
  db;
  constructor(dbPath) {
    this.db = new Database2(dbPath);
    this.db.pragma("journal_mode = WAL");
    this.db.pragma("foreign_keys = ON");
    this.initTables();
  }
  // ---------------------------------------------------------------------------
  // System settings
  // ---------------------------------------------------------------------------
  getSystem(key) {
    const row = this.db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key);
    if (row === void 0) return void 0;
    return JSON.parse(row.value);
  }
  setSystem(key, value) {
    this.db.prepare(
      `INSERT INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())
         ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
    ).run(key, JSON.stringify(value));
  }
  getAllSystem() {
    const rows = this.db.prepare("SELECT key, value FROM system_settings").all();
    return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
  }
  // ---------------------------------------------------------------------------
  // Addon settings
  // ---------------------------------------------------------------------------
  getAddon(addonId, key) {
    const row = this.db.prepare(
      "SELECT value FROM addon_settings WHERE addon_id = ? AND key = ?"
    ).get(addonId, key);
    if (row === void 0) return void 0;
    return JSON.parse(row.value);
  }
  setAddon(addonId, key, value) {
    this.db.prepare(
      `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
         ON CONFLICT(addon_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
    ).run(addonId, key, JSON.stringify(value));
  }
  getAllAddon(addonId) {
    const rows = this.db.prepare(
      "SELECT key, value FROM addon_settings WHERE addon_id = ?"
    ).all(addonId);
    return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
  }
  /** Bulk-replace all keys for an addon (within a transaction). */
  setAllAddon(addonId, config) {
    const deleteStmt = this.db.prepare("DELETE FROM addon_settings WHERE addon_id = ?");
    const insertStmt = this.db.prepare(
      `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())`
    );
    this.db.transaction(() => {
      deleteStmt.run(addonId);
      for (const [key, value] of Object.entries(config)) {
        insertStmt.run(addonId, key, JSON.stringify(value));
      }
    })();
  }
  // ---------------------------------------------------------------------------
  // Provider settings
  // ---------------------------------------------------------------------------
  getProvider(providerId, key) {
    const row = this.db.prepare(
      "SELECT value FROM provider_settings WHERE provider_id = ? AND key = ?"
    ).get(providerId, key);
    if (row === void 0) return void 0;
    return JSON.parse(row.value);
  }
  setProvider(providerId, key, value) {
    this.db.prepare(
      `INSERT INTO provider_settings (provider_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
         ON CONFLICT(provider_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
    ).run(providerId, key, JSON.stringify(value));
  }
  getAllProvider(providerId) {
    const rows = this.db.prepare(
      "SELECT key, value FROM provider_settings WHERE provider_id = ?"
    ).all(providerId);
    return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
  }
  // ---------------------------------------------------------------------------
  // Device settings
  // ---------------------------------------------------------------------------
  getDevice(deviceId, key) {
    const row = this.db.prepare(
      "SELECT value FROM device_settings WHERE device_id = ? AND key = ?"
    ).get(deviceId, key);
    if (row === void 0) return void 0;
    return JSON.parse(row.value);
  }
  setDevice(deviceId, key, value) {
    this.db.prepare(
      `INSERT INTO device_settings (device_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
         ON CONFLICT(device_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
    ).run(deviceId, key, JSON.stringify(value));
  }
  getAllDevice(deviceId) {
    const rows = this.db.prepare(
      "SELECT key, value FROM device_settings WHERE device_id = ?"
    ).all(deviceId);
    return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
  }
  // ---------------------------------------------------------------------------
  // Lifecycle
  // ---------------------------------------------------------------------------
  /** Close the SQLite connection (call on shutdown). */
  close() {
    this.db.close();
  }
  /** Check if system_settings is empty (used for first-boot seeding). */
  isSystemSettingsEmpty() {
    const row = this.db.prepare("SELECT COUNT(*) AS cnt FROM system_settings").get();
    return (row?.cnt ?? 0) === 0;
  }
  /** Seed system_settings with RUNTIME_DEFAULTS (only on first boot). */
  seedDefaults() {
    const insert = this.db.prepare(
      `INSERT OR IGNORE INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())`
    );
    this.db.transaction(() => {
      for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) {
        insert.run(key, JSON.stringify(value));
      }
    })();
  }
  // ---------------------------------------------------------------------------
  // Private helpers
  // ---------------------------------------------------------------------------
  initTables() {
    this.db.transaction(() => {
      for (const stmt of CORE_TABLE_DDL) {
        this.db.prepare(stmt).run();
      }
    })();
  }
};

export {
  FileSystemStorage,
  SqliteStorageProvider,
  SqliteStorageAddon,
  CORE_TABLE_DDL,
  addonTableToDdl,
  SettingsStore
};
//# sourceMappingURL=chunk-R3DIIBBX.mjs.map