/**
 * Database Module
 *
 * Uses SQLocal directly with BroadcastChannel for cross-tab coordination.
 *
 * Why this approach instead of SharedWorker?
 * - SQLocal already uses its own internal worker for OPFS access
 * - Wrapping it in another SharedWorker adds complexity and causes issues
 * - BroadcastChannel provides simple cross-tab communication
 * - Each tab has its own SQLocal instance but they share the same OPFS database file
 *
 * Usage:
 *   import { sql, dbReady, addLocation, getLocations } from './database.js';
 *
 *   await dbReady;
 *   await addLocation('Point A', -1.5, 7.5);
 *   const locations = await getLocations();
 */

import { SQLocal } from 'sqlocal';

// Database configuration
const DATABASE_PATH = 'lupmis2.db';
const BROADCAST_CHANNEL = 'lupmis-db-sync';

// Create SQLocal instance
const db = new SQLocal(DATABASE_PATH);

// Get the sql tagged template function
const { sql } = db;

console.log('[Database] SQLocal instance created for:', DATABASE_PATH);

// Export sql for direct queries
export { sql };

// Create broadcast channel for cross-tab coordination
const channel = new BroadcastChannel(BROADCAST_CHANNEL);

// Track if database is ready
let isReady = false;
let readyResolve;
let readyReject;

export const dbReady = new Promise((resolve, reject) => {
  readyResolve = resolve;
  readyReject = reject;
});

// Database change listeners
const changeListeners = new Set();

/**
 * Subscribe to database changes (from any tab)
 * @param {Function} listener - Called with { table, action, id }
 * @returns {Function} Unsubscribe function
 */
export function onDatabaseChange(listener) {
  changeListeners.add(listener);
  return () => changeListeners.delete(listener);
}

// Handle messages from other tabs
channel.onmessage = (event) => {
  const { type, payload } = event.data;
  if (type === 'DB_CHANGE') {
    // Notify local listeners about changes from other tabs
    for (const listener of changeListeners) {
      try {
        listener(payload);
      } catch (e) {
        console.error('[Database] Change listener error:', e);
      }
    }
  }
};

/**
 * Broadcast a database change to other tabs
 */
function broadcastChange(table, action, id = null) {
  channel.postMessage({
    type: 'DB_CHANGE',
    payload: { table, action, id, timestamp: Date.now() }
  });

  // Also notify local listeners
  for (const listener of changeListeners) {
    try {
      listener({ table, action, id, timestamp: Date.now(), local: true });
    } catch (e) {
      console.error('[Database] Change listener error:', e);
    }
  }
}

// ============================================================================
// Database Initialization
// ============================================================================

/**
 * Initialize the database schema
 */
export async function initSchema() {
  try {
    console.log('[Database] Initializing schema...');

    // Test connection
    const testResult = await sql`SELECT sqlite_version() as version`;
    console.log('[Database] SQLite version:', testResult[0]?.version);

    // Create locations table
    console.log('[Database] Creating locations table...');
    await sql`
      CREATE TABLE IF NOT EXISTS locations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        longitude REAL NOT NULL,
        latitude REAL NOT NULL,
        description TEXT,
        category TEXT DEFAULT 'default',
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
        synced INTEGER DEFAULT 0
      )
    `;

    // Verify table exists
    const tablesAfterLocations = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;
    console.log('[Database] Locations table exists:', tablesAfterLocations.length > 0);

    // Create sync_log table
    console.log('[Database] Creating sync_log table...');
    await sql`
      CREATE TABLE IF NOT EXISTS sync_log (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        table_name TEXT NOT NULL,
        record_id INTEGER NOT NULL,
        action TEXT NOT NULL,
        timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
        synced INTEGER DEFAULT 0
      )
    `;

    // Create remote_data cache table
    console.log('[Database] Creating remote_data table...');
    await sql`
      CREATE TABLE IF NOT EXISTS remote_data (
        key TEXT PRIMARY KEY,
        data TEXT NOT NULL,
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // Create collector_zones table for caching zone data
    console.log('[Database] Creating collector_zones table...');
    await sql`
      CREATE TABLE IF NOT EXISTS collector_zones (
        id INTEGER PRIMARY KEY,
        zone_name TEXT,
        geometry_wkt TEXT,
        properties TEXT,
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // Create parcels table — mirrors the server's spatial.lu_parcels structure
    // (land-use parcels). Attribute columns match the server 1:1 so the local
    // data viewer shows real fields rather than a JSON blob.
    //   status      — local-only flag: 'verified' = downloaded from the API,
    //                 'new' = drawn locally and pending server verification.
    //   fetched_at  — local-only cache timestamp.
    // Migrate older databases that used the previous JSON-blob schema
    // (id, geometry_wkt, properties, status, fetched_at): if the new `upn`
    // column is missing, drop the cached table so it is recreated with the
    // lu_parcels columns. Cached server parcels re-download on next load.
    console.log('[Database] Creating parcels table...');
    try {
      const cols = await sql`PRAGMA table_info(parcels)`;
      if (cols.length > 0 && !cols.some((c) => c.name === 'upn')) {
        console.log('[Database] Migrating parcels table to lu_parcels structure (dropping old cache)...');
        await sql`DROP TABLE parcels`;
      }
    } catch (e) {
      console.warn('[Database] parcels migration check failed:', e);
    }
    await sql`
      CREATE TABLE IF NOT EXISTS parcels (
        id INTEGER PRIMARY KEY,
        upn TEXT,
        style INTEGER,
        landuse TEXT,
        zone_code TEXT,
        zone_name TEXT,
        sector TEXT,
        block TEXT,
        parcel_no TEXT,
        prop_no TEXT,
        st_name TEXT,
        prop_add TEXT,
        fac_name TEXT,
        min_height INTEGER,
        max_height INTEGER,
        eff_date TEXT,
        lp_name TEXT,
        locality TEXT,
        mmda TEXT,
        last_update TEXT,
        remarks TEXT,
        geometry_wkt TEXT,
        created_at TEXT,
        updated_at TEXT,
        districtid INTEGER,
        status TEXT DEFAULT 'verified',
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // Create building_footprints table for caching footprint data
    console.log('[Database] Creating building_footprints table...');
    await sql`
      CREATE TABLE IF NOT EXISTS building_footprints (
        id INTEGER PRIMARY KEY,
        geometry_wkt TEXT,
        properties TEXT,
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // Create osm_roads table for caching the OSM road network
    console.log('[Database] Creating osm_roads table...');
    await sql`
      CREATE TABLE IF NOT EXISTS osm_roads (
        osm_id INTEGER PRIMARY KEY,
        geometry_wkt TEXT,
        properties TEXT,
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // UPN-grid table — district sub-division grid (one cell per UPN prefix).
    // The grid is static per district, so the cache is populated once and only
    // refreshed when the user is associated with a different district. The
    // districtid column lets getLocalUpnGrid() detect that mismatch.
    console.log('[Database] Creating upn_grid table...');
    await sql`
      CREATE TABLE IF NOT EXISTS upn_grid (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        districtid INTEGER,
        upn_prefix TEXT,
        geometry_wkt TEXT,
        fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    // ── GPS trails ──────────────────────────────────────────────────────
    // Recorded field-movement tracks. These are the local store for the
    // reusable GeoTracker module (src/geotracker/). `client_uuid` lets the
    // server de-duplicate re-synced trails; `satellites` is nullable because
    // the web Geolocation API does not expose it (only native builds do).
    console.log('[Database] Creating gps_trails table...');
    await sql`
      CREATE TABLE IF NOT EXISTS gps_trails (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        client_uuid TEXT UNIQUE,
        name TEXT,
        district_id TEXT,
        started_at TEXT NOT NULL,
        ended_at TEXT,
        status TEXT NOT NULL DEFAULT 'recording',
        point_count INTEGER NOT NULL DEFAULT 0,
        distance_m REAL NOT NULL DEFAULT 0,
        synced INTEGER NOT NULL DEFAULT 0,
        remote_id TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    `;

    console.log('[Database] Creating gps_trail_points table...');
    await sql`
      CREATE TABLE IF NOT EXISTS gps_trail_points (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        trail_id INTEGER NOT NULL,
        seq INTEGER NOT NULL,
        longitude REAL NOT NULL,
        latitude REAL NOT NULL,
        altitude REAL,
        accuracy REAL,
        altitude_accuracy REAL,
        heading REAL,
        speed REAL,
        satellites INTEGER,
        recorded_at TEXT NOT NULL
      )
    `;

    // ── External imports ────────────────────────────────────────────────
    // Staging for shapefile / GeoJSON / KML imports before they are uploaded
    // to the server. See LUPMIS2_Import_Upload_Design.docx §4.
    //   external_imports          — one row per imported file (the dataset)
    //   external_import_features  — staged features (rows pending upload)
    //
    // Status state machine (aligns with the database team's staged-upload
    // model — uploads land in spatial.lu_parcels_upload_tmp first, then a
    // supervisor promotes them to the live spatial.lu_parcels via the
    // migrated flag):
    //
    //   'imported' → 'mapped' → 'uploading' → 'submitted' → 'migrated'
    //                                       ↘ 'failed'
    //
    // 'other' is a terminal display-only state (user chose "Other / view only").
    // 'submitted' = row exists in upload_tmp, awaiting supervisor review.
    // 'migrated'  = supervisor approved & promoted to the live table.
    console.log('[Database] Creating external_imports table...');
    await sql`
      CREATE TABLE IF NOT EXISTS external_imports (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        filename TEXT NOT NULL,
        target_type TEXT NOT NULL DEFAULT 'other',
        mapping_json TEXT,
        status TEXT NOT NULL DEFAULT 'imported',
        feature_count INTEGER NOT NULL DEFAULT 0,
        error_count INTEGER NOT NULL DEFAULT 0,
        client_import_id TEXT UNIQUE,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        last_uploaded_at TEXT
      )
    `;

    console.log('[Database] Creating external_import_features table...');
    await sql`
      CREATE TABLE IF NOT EXISTS external_import_features (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        import_id INTEGER NOT NULL REFERENCES external_imports(id) ON DELETE CASCADE,
        client_uuid TEXT UNIQUE,
        geometry_wkt TEXT NOT NULL,
        properties_json TEXT,
        upload_status TEXT NOT NULL DEFAULT 'pending',
        server_id INTEGER,
        error_message TEXT
      )
    `;

    // Create indexes
    await sql`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`;
    await sql`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`;
    await sql`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`;
    await sql`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`;
    await sql`CREATE INDEX IF NOT EXISTS idx_external_imports_status ON external_imports(status)`;
    await sql`CREATE INDEX IF NOT EXISTS idx_external_import_features_import ON external_import_features(import_id, upload_status)`;

    // Final verification
    const allTables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
    console.log('[Database] All tables:', allTables.map(t => t.name));

    isReady = true;
    readyResolve(true);
    console.log('[Database] ✓ Schema initialized');

  } catch (error) {
    console.error('[Database] ✗ Schema init failed:', error);
    readyReject(error);
    throw error;
  }
}

// ============================================================================
// Location Operations
// ============================================================================

/**
 * Add a new location
 */
export async function addLocation(name, longitude, latitude, options = {}) {
  const { description = null, category = 'default' } = options;

  console.log('[Database] Adding location:', name, longitude, latitude, category);

  try {
    // Check table exists first
    const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;
    console.log('[Database] Table check before insert:', tableCheck);

    if (tableCheck.length === 0) {
      console.error('[Database] ✗ locations table does not exist!');
      throw new Error('locations table does not exist');
    }

    // Insert - using explicit values
    console.log('[Database] Executing INSERT...');
    await sql`
    INSERT INTO locations (name, longitude, latitude, description, category)
    VALUES (${name}, ${longitude}, ${latitude}, ${description}, ${category})
  `;
    console.log('[Database] INSERT completed');

    // Get the ID
    const idResult = await sql`SELECT last_insert_rowid() as id`;
    const newId = idResult[0]?.id;
    console.log('[Database] New ID:', newId);

    // Verify it was actually inserted
    const verifyResult = await sql`SELECT * FROM locations WHERE id = ${newId}`;
    console.log('[Database] Verify insert:', verifyResult);

    if (verifyResult.length === 0) {
      console.error('[Database] ✗ Insert verification failed - row not found!');
      throw new Error('Insert verification failed');
    }

  // Log for sync
    await sql`
      INSERT INTO sync_log (table_name, record_id, action)
      VALUES ('locations', ${newId}, 'INSERT')
    `;

  // Broadcast to other tabs
  broadcastChange('locations', 'INSERT', newId);

    console.log('[Database] ✓ Location added:', newId);
  return { id: newId };

  } catch (error) {
    console.error('[Database] ✗ Failed to add location:', error);
    throw error;
}
}

/**
 * Get all locations
 */
export async function getLocations(options = {}) {
  const { category = null, limit = 1000 } = options;

  try {
    // First check if table exists
    const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;
    console.log('[Database] getLocations - table exists:', tableCheck.length > 0);

    if (tableCheck.length === 0) {
      console.warn('[Database] locations table does not exist yet');
      return [];
    }

    let results;
  if (category) {
      results = await sql`
      SELECT * FROM locations
      WHERE category = ${category}
      ORDER BY created_at DESC
      LIMIT ${limit}
    `;
    } else {
      results = await sql`
    SELECT * FROM locations
    ORDER BY created_at DESC
    LIMIT ${limit}
  `;
}

    console.log('[Database] getLocations returned', results.length, 'rows');
    return results;

  } catch (error) {
    console.error('[Database] getLocations error:', error);
    return [];
  }
}

export async function getLocation(id) {
  try {
  const results = await sql`SELECT * FROM locations WHERE id = ${id}`;
  return results[0] || null;
  } catch (error) {
    console.error('[Database] getLocation error:', error);
    return null;
}
}

/**
 * Update a location
 */
export async function updateLocation(id, updates) {
  const { name, longitude, latitude, description, category } = updates;

  try {
    const location = await getLocation(id);
    if (!location) {
      throw new Error(`Location ${id} not found`);
    }

  await sql`
    UPDATE locations
    SET
        name = ${name ?? location.name},
        longitude = ${longitude ?? location.longitude},
        latitude = ${latitude ?? location.latitude},
        description = ${description ?? location.description},
        category = ${category ?? location.category},
      updated_at = CURRENT_TIMESTAMP,
      synced = 0
    WHERE id = ${id}
  `;

  // Log for sync
  await sql`
    INSERT INTO sync_log (table_name, record_id, action)
    VALUES ('locations', ${id}, 'UPDATE')
  `;

  // Broadcast to other tabs
  broadcastChange('locations', 'UPDATE', id);
    console.log('[Database] ✓ Location updated:', id);

  } catch (error) {
    console.error('[Database] ✗ updateLocation error:', error);
    throw error;
}
}

/**
 * Delete a location
 */
export async function deleteLocation(id) {
  try {
  await sql`
    INSERT INTO sync_log (table_name, record_id, action)
    VALUES ('locations', ${id}, 'DELETE')
  `;

  await sql`DELETE FROM locations WHERE id = ${id}`;

  // Broadcast to other tabs
  broadcastChange('locations', 'DELETE', id);
    console.log('[Database] ✓ Location deleted:', id);

  } catch (error) {
    console.error('[Database] ✗ deleteLocation error:', error);
    throw error;
}
}

/**
 * Get location count
 */
export async function getLocationCount() {
  try {
  const result = await sql`SELECT COUNT(*) as count FROM locations`;
  return result[0]?.count ?? 0;
  } catch (error) {
    console.error('[Database] getLocationCount error:', error);
    return 0;
}
}

// ============================================================================
// Sync Operations
// ============================================================================

/**
 * Get unsynced changes
 */
export async function getUnsyncedChanges() {
  return sql`SELECT * FROM sync_log WHERE synced = 0 ORDER BY timestamp ASC`;
}

/**
 * Mark changes as synced
 */
export async function markSynced(syncLogIds) {
  if (!syncLogIds.length) return;
  for (const id of syncLogIds) {
    await sql`UPDATE sync_log SET synced = 1 WHERE id = ${id}`;
}
}

/**
 * Get locations that need syncing
 */
export async function getUnsyncedLocations() {
  return sql`SELECT * FROM locations WHERE synced = 0`;
}

/**
 * Mark locations as synced
 */
export async function markLocationsSynced(ids) {
  if (!ids.length) return;
  for (const id of ids) {
    await sql`UPDATE locations SET synced = 1 WHERE id = ${id}`;
}
}

// ============================================================================
// Remote Data Cache
// ============================================================================

/**
 * Save remote API data to the local cache.
 * Uses INSERT OR REPLACE so the same key is always overwritten.
 *
 * @param {string} key - Unique identifier (e.g. 'district_boundary')
 * @param {Object|Array} data - JSON-serialisable data to cache
 */
export async function saveRemoteData(key, data) {
  try {
    const json = JSON.stringify(data);
    await sql`
      INSERT OR REPLACE INTO remote_data (key, data, fetched_at)
      VALUES (${key}, ${json}, CURRENT_TIMESTAMP)
    `;
    console.log('[Database] ✓ Remote data cached:', key);
  } catch (error) {
    console.error('[Database] ✗ Failed to cache remote data:', key, error);
    throw error;
  }
}

/**
 * Retrieve cached remote data by key.
 *
 * @param {string} key - Unique identifier (e.g. 'district_boundary')
 * @returns {Promise<Object|null>} Parsed data, or null if not cached
 */
export async function getRemoteData(key) {
  try {
    const rows = await sql`SELECT data, fetched_at FROM remote_data WHERE key = ${key}`;
    if (rows.length === 0) return null;
    const parsed = JSON.parse(rows[0].data);
    console.log('[Database] ✓ Remote data loaded from cache:', key, '(fetched', rows[0].fetched_at + ')');
    return parsed;
  } catch (error) {
    console.error('[Database] ✗ Failed to read cached remote data:', key, error);
    return null;
  }
}

// ============================================================================
// Collector Zones
// ============================================================================

/**
 * Save collector zones to the local table.
 * Replaces all existing rows.
 *
 * @param {Array} zones - Array of zone objects from the API
 */
export async function saveCollectorZones(zones) {
  try {
    await sql`DELETE FROM collector_zones`;
    for (const z of zones) {
      const props = JSON.stringify(z);
      await sql`
        INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)
        VALUES (${z.colzonenr || z.id}, ${z.colzonename || z.zone_name || ''}, ${z.polygon || z.boundary || ''}, ${props}, CURRENT_TIMESTAMP)
      `;
    }
    console.log('[Database] ✓ Saved', zones.length, 'collector zones');
  } catch (error) {
    console.error('[Database] ✗ Failed to save collector zones:', error);
    throw error;
  }
}

/**
 * Load all cached collector zones from the local table.
 * @returns {Promise<Array|null>} Array of zone objects, or null if empty
 */
export async function getLocalCollectorZones() {
  try {
    const rows = await sql`SELECT properties FROM collector_zones ORDER BY id`;
    if (rows.length === 0) return null;
    return rows.map(r => JSON.parse(r.properties));
  } catch (error) {
    console.error('[Database] ✗ Failed to read local collector zones:', error);
    return null;
  }
}

// ============================================================================
// UPN Grid
// ============================================================================

/**
 * Save the UPN-grid for one district to the local table. Replaces any
 * previously cached grid (even if it was for a different district) — the
 * grid is static per district, so we only ever keep the current district's
 * cells locally.
 *
 * @param {Array} rows       - Grid rows from the API
 *                             (each: { polygon, districtid, upn_prefix })
 * @param {number|string} districtId - The current district id
 */
export async function saveUpnGrid(rows, districtId) {
  try {
    await sql`BEGIN`;
    await sql`DELETE FROM upn_grid`;
    let saved = 0;
    for (const r of rows) {
      const wkt = r.polygon || r.geometry_wkt || r.geom || '';
      await sql`
        INSERT INTO upn_grid (districtid, upn_prefix, geometry_wkt, fetched_at)
        VALUES (${numOrNull(districtId)}, ${r.upn_prefix ?? null}, ${wkt}, CURRENT_TIMESTAMP)
      `;
      saved++;
    }
    await sql`COMMIT`;
    console.log('[Database] ✓ Saved', saved, 'UPN-grid cells (district', districtId, ')');
  } catch (error) {
    try { await sql`ROLLBACK`; } catch { /* no active txn */ }
    console.error('[Database] ✗ Failed to save UPN grid:', error);
    throw error;
  }
}

/**
 * Load the UPN-grid rows that belong to the given district. Returns null
 * if no cells are cached for that district — the caller should then fetch
 * from the API and call saveUpnGrid().
 *
 * @param {number|string} districtId
 * @returns {Promise<Array|null>}
 */
export async function getLocalUpnGrid(districtId) {
  try {
    const rows = await sql`
      SELECT id, districtid, upn_prefix, geometry_wkt
      FROM upn_grid
      WHERE districtid = ${numOrNull(districtId)}
      ORDER BY id
    `;
    if (rows.length === 0) return null;
    return rows;
  } catch (error) {
    console.error('[Database] ✗ Failed to read UPN grid:', error);
    return null;
  }
}

// ============================================================================
// External imports (staging for shapefile / GeoJSON / KML uploads)
// See LUPMIS2_Import_Upload_Design.docx §3 & §4.
// ============================================================================

/** Tiny RFC-4122-ish UUIDv4 (uses crypto.randomUUID when available). */
function uuidv4() {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID();
  }
  // Fallback for older browsers
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  });
}

/**
 * Create a new staging row for an imported dataset. Returns the import_id.
 *
 * @param {Object} opts
 * @param {string} opts.filename       — original filename
 * @param {string} [opts.targetType]   — initial target ('other' by default)
 * @param {number} [opts.featureCount] — number of features being staged
 * @returns {Promise<{ id: number, client_import_id: string }>}
 */
export async function createExternalImport(opts) {
  const { filename, targetType = 'other', featureCount = 0 } = opts;
  const clientImportId = uuidv4();
  try {
    await sql`
      INSERT INTO external_imports
        (filename, target_type, status, feature_count, client_import_id)
      VALUES
        (${filename}, ${targetType},
         ${targetType === 'other' ? 'other' : 'imported'},
         ${featureCount}, ${clientImportId})
    `;
    const idRow = await sql`SELECT last_insert_rowid() AS id`;
    const id = idRow[0]?.id;
    broadcastChange('external_imports', 'INSERT', id);
    return { id, client_import_id: clientImportId };
  } catch (error) {
    console.error('[Database] ✗ Failed to create external import:', error);
    throw error;
  }
}

/**
 * Bulk-insert staging features for an import. Wraps the inserts in a single
 * transaction (the upload payload is typically dozens to thousands of rows).
 *
 * @param {number} importId
 * @param {Array<{ geometry_wkt: string, properties: Object }>} features
 */
export async function addExternalImportFeatures(importId, features) {
  if (!Array.isArray(features) || features.length === 0) return 0;
  try {
    await sql`BEGIN`;
    let inserted = 0;
    for (const f of features) {
      const wkt = f.geometry_wkt || '';
      if (!wkt) continue;
      const propsJson = JSON.stringify(f.properties ?? {});
      const cuuid = f.client_uuid || uuidv4();
      await sql`
        INSERT INTO external_import_features
          (import_id, client_uuid, geometry_wkt, properties_json, upload_status)
        VALUES
          (${importId}, ${cuuid}, ${wkt}, ${propsJson}, 'pending')
      `;
      inserted++;
    }
    await sql`COMMIT`;
    broadcastChange('external_import_features', 'INSERT', importId);
    return inserted;
  } catch (error) {
    try { await sql`ROLLBACK`; } catch { /* no active txn */ }
    console.error('[Database] ✗ Failed to add import features:', error);
    throw error;
  }
}

/**
 * Update target type + mapping + status for a staged import. Pass any
 * subset of fields. Useful targets:
 *   updateExternalImport(id, { targetType: 'parcels', mapping: {...},
 *                              status: 'mapped' })
 */
export async function updateExternalImport(importId, patch = {}) {
  try {
    const cur = await sql`SELECT * FROM external_imports WHERE id = ${importId}`;
    if (cur.length === 0) throw new Error(`Import ${importId} not found`);
    const row = cur[0];

    const targetType  = patch.targetType  ?? row.target_type;
    const mappingJson = patch.mapping !== undefined
      ? (patch.mapping ? JSON.stringify(patch.mapping) : null)
      : row.mapping_json;
    const status      = patch.status      ?? row.status;
    const errorCount  = patch.errorCount  ?? row.error_count;
    const lastUploadedAt = patch.lastUploadedAt ?? row.last_uploaded_at;

    await sql`
      UPDATE external_imports SET
        target_type = ${targetType},
        mapping_json = ${mappingJson},
        status = ${status},
        error_count = ${errorCount},
        last_uploaded_at = ${lastUploadedAt}
      WHERE id = ${importId}
    `;
    broadcastChange('external_imports', 'UPDATE', importId);
  } catch (error) {
    console.error('[Database] ✗ Failed to update external import:', error);
    throw error;
  }
}

/** Get an external_imports row by id (parsed mapping_json). */
export async function getExternalImport(importId) {
  try {
    const rows = await sql`SELECT * FROM external_imports WHERE id = ${importId}`;
    if (rows.length === 0) return null;
    const row = rows[0];
    return { ...row, mapping: row.mapping_json ? JSON.parse(row.mapping_json) : null };
  } catch (error) {
    console.error('[Database] ✗ Failed to read external import:', error);
    return null;
  }
}

/** List every external import, newest first. */
export async function listExternalImports() {
  try {
    return await sql`SELECT * FROM external_imports ORDER BY id DESC`;
  } catch (error) {
    console.error('[Database] ✗ Failed to list external imports:', error);
    return [];
  }
}

/** Get the staging features for an import (parsed properties_json). */
export async function getExternalImportFeatures(importId) {
  try {
    const rows = await sql`
      SELECT id, client_uuid, geometry_wkt, properties_json,
             upload_status, server_id, error_message
      FROM external_import_features
      WHERE import_id = ${importId}
      ORDER BY id
    `;
    return rows.map((r) => ({
      ...r,
      properties: r.properties_json ? JSON.parse(r.properties_json) : {},
    }));
  } catch (error) {
    console.error('[Database] ✗ Failed to read import features:', error);
    return [];
  }
}

/**
 * Bulk-rewrite the properties_json of every staged feature for an import.
 * Used after the user confirms a field-mapping: the caller passes a function
 * that receives each row's parsed properties and returns the remapped ones.
 * All updates run in a single transaction.
 *
 * @param {number} importId
 * @param {(props: Object) => Object} remap
 */
export async function remapImportedFeatureProperties(importId, remap) {
  try {
    const rows = await sql`
      SELECT id, properties_json
      FROM external_import_features
      WHERE import_id = ${importId}
    `;
    if (rows.length === 0) return 0;

    await sql`BEGIN`;
    let n = 0;
    for (const r of rows) {
      const before = r.properties_json ? JSON.parse(r.properties_json) : {};
      const after  = remap(before) ?? {};
      await sql`
        UPDATE external_import_features
        SET properties_json = ${JSON.stringify(after)}
        WHERE id = ${r.id}
      `;
      n++;
    }
    await sql`COMMIT`;
    broadcastChange('external_import_features', 'UPDATE', importId);
    return n;
  } catch (error) {
    try { await sql`ROLLBACK`; } catch { /* */ }
    console.error('[Database] ✗ Failed to remap import features:', error);
    throw error;
  }
}

/**
 * Update a single staged feature's geometry (and properties, optionally).
 * Used when the user modifies an imported feature on the map (the EditBar
 * Modify interaction fires `modifyend`) — we write the new WKT back so the
 * next upload re-sends the corrected geometry.
 *
 * @param {string} clientUuid - the per-feature idempotency key on the OL feature
 * @param {string} geometryWkt - new geometry in EPSG:4326 WKT
 * @param {Object} [properties] - optional new properties (if omitted, leave as-is)
 */
export async function updateExternalImportFeatureGeometry(clientUuid, geometryWkt, properties) {
  try {
    if (properties !== undefined) {
      const propsJson = JSON.stringify(properties);
      await sql`
        UPDATE external_import_features
        SET geometry_wkt = ${geometryWkt}, properties_json = ${propsJson}
        WHERE client_uuid = ${clientUuid}
      `;
    } else {
      await sql`
        UPDATE external_import_features
        SET geometry_wkt = ${geometryWkt}
        WHERE client_uuid = ${clientUuid}
      `;
    }
    broadcastChange('external_import_features', 'UPDATE', clientUuid);
  } catch (error) {
    console.error('[Database] ✗ Failed to update import feature geometry:', error);
    throw error;
  }
}

/**
 * Delete a single staged feature by client_uuid, and decrement the parent
 * import's feature_count. Used when the user deletes an imported feature on
 * the map (EditBar Delete).
 *
 * @param {string} clientUuid
 */
export async function deleteExternalImportFeature(clientUuid) {
  try {
    // Look up parent import_id first so we can decrement its count atomically.
    const row = await sql`
      SELECT import_id FROM external_import_features WHERE client_uuid = ${clientUuid}
    `;
    if (row.length === 0) return; // already gone
    const importId = row[0].import_id;

    await sql`BEGIN`;
    try {
      await sql`DELETE FROM external_import_features WHERE client_uuid = ${clientUuid}`;
      await sql`
        UPDATE external_imports
        SET feature_count = MAX(0, feature_count - 1)
        WHERE id = ${importId}
      `;
      await sql`COMMIT`;
    } catch (e) {
      try { await sql`ROLLBACK`; } catch { /* */ }
      throw e;
    }
    broadcastChange('external_import_features', 'DELETE', clientUuid);
    broadcastChange('external_imports', 'UPDATE', importId);
  } catch (error) {
    console.error('[Database] ✗ Failed to delete import feature:', error);
    throw error;
  }
}

/** Delete a staged import (and its features via CASCADE). */
export async function deleteExternalImport(importId) {
  try {
    await sql`DELETE FROM external_imports WHERE id = ${importId}`;
    broadcastChange('external_imports', 'DELETE', importId);
  } catch (error) {
    console.error('[Database] ✗ Failed to delete external import:', error);
    throw error;
  }
}

// ============================================================================
// Parcels
// ============================================================================

/** Coerce a value for an INTEGER column; '' / null / undefined / NaN → null. */
function numOrNull(v) {
  if (v === '' || v === null || v === undefined) return null;
  const n = Number(v);
  return Number.isNaN(n) ? null : n;
}

/**
 * Save parcels to the local table (mirrors spatial.lu_parcels).
 * Replaces all server-cached rows. Each API field maps to its own column.
 * Geometry is taken from the server `geom` field (WKT, EPSG:4326).
 *
 * @param {Array} parcels - Array of parcel objects from the API
 */
export async function saveParcels(parcels) {
  try {
    // Wrap the bulk load in a single transaction — with ~25k parcels, per-row
    // auto-commits on OPFS would be prohibitively slow.
    await sql`BEGIN`;
    await sql`DELETE FROM parcels`;
    let saved = 0;
    for (const p of parcels) {
      const id = p.id ?? p.parcelid ?? p.parcel_id ?? null;
      if (id == null) continue; // skip rows without a usable ID
      // Geometry must be a WKT *string* for the geometry_wkt TEXT column.
      // The API sends WKT in `boundary` and a GeoJSON *object* in `geom`, so
      // prefer the string fields and only accept `geom` when it is a string.
      const wkt = p.boundary || p.geometry_wkt || p.polygon || p.wkt
        || (typeof p.geom === 'string' ? p.geom : '');
      await sql`
        INSERT OR REPLACE INTO parcels (
          id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,
          prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,
          lp_name, locality, mmda, last_update, remarks, geometry_wkt,
          created_at, updated_at, districtid, status, fetched_at
        ) VALUES (
          ${id}, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null},
          ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null},
          ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null},
          ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null},
          ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null},
          ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null},
          ${p.last_update ?? null}, ${p.remarks ?? null}, ${wkt},
          ${p.created_at ?? null}, ${p.updated_at ?? null}, ${numOrNull(p.districtid)},
          'verified', CURRENT_TIMESTAMP
        )
      `;
      saved++;
    }
    await sql`COMMIT`;
    console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'skipped/replaced)');
  } catch (error) {
    try { await sql`ROLLBACK`; } catch { /* no active txn */ }
    console.error('[Database] ✗ Failed to save parcels:', error);
    throw error;
  }
}

/**
 * Load all cached parcels from the local table.
 * Each row is a plain object keyed by column name (incl. geometry_wkt).
 * @returns {Promise<Array|null>} Array of parcel rows, or null if empty
 */
export async function getLocalParcels() {
  try {
    const rows = await sql`SELECT * FROM parcels ORDER BY id`;
    if (rows.length === 0) return null;
    return rows;
  } catch (error) {
    console.error('[Database] ✗ Failed to read local parcels:', error);
    return null;
  }
}

/**
 * Update a single parcel's attribute columns in the local table.
 * Geometry, id, created_at and status are left unchanged; updated_at is bumped.
 *
 * @param {number|string} parcelId - The parcel id
 * @param {Object} p - Updated attribute values (keys = lu_parcels columns)
 */
export async function updateParcel(parcelId, p) {
  try {
    await sql`
      UPDATE parcels SET
        upn = ${p.upn ?? null},
        style = ${numOrNull(p.style)},
        landuse = ${p.landuse ?? null},
        zone_code = ${p.zone_code ?? null},
        zone_name = ${p.zone_name ?? null},
        sector = ${p.sector ?? null},
        block = ${p.block ?? null},
        parcel_no = ${p.parcel_no ?? null},
        prop_no = ${p.prop_no ?? null},
        st_name = ${p.st_name ?? null},
        prop_add = ${p.prop_add ?? null},
        fac_name = ${p.fac_name ?? null},
        min_height = ${numOrNull(p.min_height)},
        max_height = ${numOrNull(p.max_height)},
        eff_date = ${p.eff_date ?? null},
        lp_name = ${p.lp_name ?? null},
        locality = ${p.locality ?? null},
        mmda = ${p.mmda ?? null},
        last_update = ${p.last_update ?? null},
        remarks = ${p.remarks ?? null},
        districtid = ${numOrNull(p.districtid)},
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ${parcelId}
    `;
    console.log('[Database] ✓ Parcel updated:', parcelId);
    broadcastChange('parcels', 'UPDATE', parcelId);
  } catch (error) {
    console.error('[Database] ✗ Failed to update parcel:', parcelId, error);
    throw error;
  }
}

/**
 * Insert a newly drawn parcel into the local table.
 * The parcel is tagged with status='new' to indicate it needs verification.
 *
 * @param {string} geometryWkt - WKT geometry string (EPSG:4326)
 * @param {Object} p - Attribute values from the form (keys = lu_parcels columns)
 * @returns {Promise<{id: number}>} The new row id
 */
export async function insertNewParcel(geometryWkt, p = {}) {
  try {
    await sql`
      INSERT INTO parcels (
        id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,
        prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,
        lp_name, locality, mmda, last_update, remarks, geometry_wkt,
        created_at, updated_at, districtid, status, fetched_at
      ) VALUES (
        NULL, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null},
        ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null},
        ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null},
        ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null},
        ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null},
        ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null},
        ${p.last_update ?? null}, ${p.remarks ?? null}, ${geometryWkt},
        CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${numOrNull(p.districtid)},
        'new', CURRENT_TIMESTAMP
      )
    `;
    const idResult = await sql`SELECT last_insert_rowid() as id`;
    const newId = idResult[0]?.id;
    console.log('[Database] ✓ New parcel inserted:', newId, '(status: new)');
    broadcastChange('parcels', 'INSERT', newId);
    return { id: newId };
  } catch (error) {
    console.error('[Database] ✗ Failed to insert new parcel:', error);
    throw error;
  }
}

// ============================================================================
// Building Footprints
// ============================================================================

/**
 * Save building footprints to the local table.
 * Replaces all existing rows.
 *
 * @param {Array} footprints - Array of footprint objects from the API
 */
export async function saveBuildingFootprints(footprints) {
  try {
    // Log first entry's keys and value types to help debug field names
    if (footprints.length > 0) {
      const first = footprints[0];
      const types = {};
      for (const [k, v] of Object.entries(first)) {
        types[k] = v === null ? 'null' : typeof v;
      }
      console.log('[Database] First footprint field types:', types);
    }

    await sql`DELETE FROM building_footprints`;
    for (const f of footprints) {
      const props = JSON.stringify(f);

      // Geometry may arrive as a string (WKT) or an object (GeoJSON) —
      // coerce to a string so SQLocal can bind it.
      let rawWkt = f.polygon || f.boundary || f.geom || f.wkt || f.footprint || '';
      const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);

      // ID must be a primitive (number or null)
      let rawId = f.id || f.footprint_id || f.building_id || null;
      const id = (rawId !== null && typeof rawId === 'object') ? null : rawId;

      await sql`
        INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
        VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP)
      `;
    }
    console.log('[Database] ✓ Saved', footprints.length, 'building footprints');
  } catch (error) {
    console.error('[Database] ✗ Failed to save building footprints:', error);
    throw error;
  }
}

/**
 * Load all cached building footprints from the local table.
 * @returns {Promise<Array|null>} Array of footprint objects, or null if empty
 */
export async function getLocalBuildingFootprints() {
  try {
    const rows = await sql`SELECT properties FROM building_footprints ORDER BY id`;
    if (rows.length === 0) return null;
    return rows.map(r => JSON.parse(r.properties));
  } catch (error) {
    console.error('[Database] ✗ Failed to read local building footprints:', error);
    return null;
  }
}

/**
 * Save OSM roads to the local SQLite table.
 * Replaces all existing rows.
 *
 * @param {Array} roads - Array of road objects from the API
 */
export async function saveOSMRoads(roads) {
  try {
    if (roads.length > 0) {
      const first = roads[0];
      const types = {};
      for (const [k, v] of Object.entries(first)) {
        types[k] = v === null ? 'null' : typeof v;
      }
      console.log('[Database] First road field types:', types);
    }

    await sql`DELETE FROM osm_roads`;
    for (const r of roads) {
      const props = JSON.stringify(r);

      // Geometry — may arrive as WKT string or GeoJSON object
      let rawWkt = r.geom || r.geometry || r.wkt || r.road || r.line || '';
      const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);

      // osm_id must be a primitive — fall back to null if missing or malformed
      let rawId = r.osm_id ?? r.osmid ?? r.id ?? null;
      const osmId = (rawId !== null && typeof rawId === 'object') ? null : rawId;

      await sql`
        INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
        VALUES (${osmId}, ${wkt}, ${props}, CURRENT_TIMESTAMP)
      `;
    }
    console.log('[Database] ✓ Saved', roads.length, 'OSM roads');
  } catch (error) {
    console.error('[Database] ✗ Failed to save OSM roads:', error);
    throw error;
  }
}

/**
 * Load all cached OSM roads from the local table.
 * @returns {Promise<Array|null>} Array of road objects, or null if empty
 */
export async function getLocalOSMRoads() {
  try {
    const rows = await sql`SELECT properties FROM osm_roads ORDER BY osm_id`;
    if (rows.length === 0) return null;
    return rows.map(r => JSON.parse(r.properties));
  } catch (error) {
    console.error('[Database] ✗ Failed to read local OSM roads:', error);
    return null;
  }
}

// ============================================================================
// Export / Import
// ============================================================================

/**
 * Export database for backup
 */
export async function exportDatabase() {
  return db.getDatabaseFile();
}

/**
 * Import database from backup
 */
export async function importDatabase(data) {
  await db.overwriteDatabaseFile(data);
  broadcastChange('*', 'IMPORT', null);
}

/**
 * Download database as file
 */
export async function downloadDatabase(filename = 'lupmis-backup.sqlite3') {
  const data = await exportDatabase();
  const blob = new Blob([data], { type: 'application/x-sqlite3' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  URL.revokeObjectURL(url);
}

// Export to GeoJSON
export async function exportToGeoJSON() {
  const locations = await getLocations();

  return {
    type: 'FeatureCollection',
    features: locations.map((loc) => ({
      type: 'Feature',
      properties: {
        id: loc.id,
        name: loc.name,
        category: loc.category,
        notes: loc.notes,
        created_at: loc.created_at,
      },
      geometry: {
        type: 'Point',
        coordinates: [loc.lon, loc.lat],
      },
    })),
  };
}

// ============================================================================
// Utility & Debug
// ============================================================================

/**
 * Get database status
 */
export async function getDatabaseStatus() {
  try {
  const tables = await sql`
    SELECT name FROM sqlite_master
    WHERE type='table' AND name NOT LIKE 'sqlite_%'
    ORDER BY name
  `;

  const locationCount = await getLocationCount();

  return {
    ready: isReady,
    databasePath: DATABASE_PATH,
    tables: tables.map(t => t.name),
    locationCount
  };
  } catch (error) {
    return {
      ready: false,
      error: error.message
    };
}
}

// ============================================================================
// Cached-Layer Management
// ============================================================================

/**
 * Tables that hold data fetched from the server.
 *
 * Clearing these is safe — the corresponding loaders (loadParcels,
 * loadBuildingFootprints, loadOSMRoads, loadCollectorZones, …) will re-fetch
 * the data from the API on the next app start.
 *
 * NOT included: user-created tables (`locations`, `pending_changes`) — those
 * hold local work that must not be auto-deleted.
 */
export const CACHED_LAYER_TABLES = Object.freeze([
  'parcels',
  'building_footprints',
  'osm_roads',
  'collector_zones',
  'upn_grid',
  'remote_data',
]);

/**
 * Check whether a table name is in the cleared-layer allow-list.
 * @param {string} tableName
 * @returns {boolean}
 */
export function isCachedLayerTable(tableName) {
  return CACHED_LAYER_TABLES.includes(tableName);
}

/**
 * Delete all rows from a single cached-layer table.
 * Rejects unknown table names so this can't be abused to drop user data.
 *
 * @param {string} tableName - One of CACHED_LAYER_TABLES
 * @returns {Promise<number>} Number of rows that were in the table before deletion
 */
export async function clearTable(tableName) {
  if (!isCachedLayerTable(tableName)) {
    throw new Error(`Refusing to clear "${tableName}" — not a known cached-layer table`);
  }

  const before = await sql(`SELECT COUNT(*) AS n FROM "${tableName}"`);
  const count = before[0]?.n ?? 0;

  await sql(`DELETE FROM "${tableName}"`);
  console.log(`[Database] ✓ Cleared "${tableName}" (${count} rows)`);
  broadcastChange(tableName, 'CLEAR', null);
  return count;
}

/**
 * Clear every cached-layer table (whatever exists in this database).
 * Tables that don't exist yet are skipped silently.
 *
 * @returns {Promise<{ table: string, count: number }[]>} per-table report
 */
export async function clearAllCachedLayers() {
  const existing = await sql`
    SELECT name FROM sqlite_master
    WHERE type='table' AND name IN (
      'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'upn_grid', 'remote_data'
    )
  `;
  const existingNames = new Set(existing.map((r) => r.name));

  const results = [];
  for (const tableName of CACHED_LAYER_TABLES) {
    if (!existingNames.has(tableName)) continue;
    try {
      const count = await clearTable(tableName);
      results.push({ table: tableName, count });
    } catch (err) {
      console.error(`[Database] Failed to clear ${tableName}:`, err);
      results.push({ table: tableName, count: 0, error: err.message });
    }
  }

  const total = results.reduce((s, r) => s + r.count, 0);
  console.log(`[Database] ✓ Cleared all cached layers: ${total} rows across ${results.length} tables`);
  return results;
}

/**
 * Get a list of all tables with their row counts.
 * @returns {Promise<Array<{name: string, count: number}>>}
 */
export async function getTableStats() {
  const tables = await sql`
    SELECT name FROM sqlite_master
    WHERE type='table' AND name NOT LIKE 'sqlite_%'
    ORDER BY name
  `;

  if (tables.length === 0) return [];

  // Build a UNION ALL query — table names come from sqlite_master so safe to embed.
  // sql() accepts a plain string as first argument (not only tagged template).
  const query = tables
    .map(t => `SELECT '${t.name}' AS name, COUNT(*) AS count FROM "${t.name}"`)
    .join(' UNION ALL ');

  return sql(query);
}

/**
 * Get all rows from a given table (max 200 rows for safety).
 * Table name is validated against sqlite_master before querying.
 * @param {string} tableName - Name of the table to read
 * @param {number} [limit=200] - Max rows to return
 * @returns {Promise<{columns: string[], rows: Object[]}>}
 */
export async function getTableContent(tableName, limit = 200) {
  // Validate that the table actually exists (prevent injection)
  const valid = await sql`
    SELECT name FROM sqlite_master
    WHERE type='table' AND name = ${tableName}
  `;
  if (valid.length === 0) {
    throw new Error(`Table "${tableName}" does not exist`);
  }

  const rows = await sql(`SELECT * FROM "${tableName}" LIMIT ${limit}`);

  // Extract column names from the first row (or return empty)
  const columns = rows.length > 0 ? Object.keys(rows[0]) : [];

  return { columns, rows };
}

// Debug function - call from console to test
export async function testDatabase() {
  console.log('=== DATABASE TEST ===');

  try {
    // 1. Check connection
    const version = await sql`SELECT sqlite_version() as v`;
    console.log('1. SQLite version:', version[0].v);

    // 2. Check tables
    const tables = await sql`SELECT name FROM sqlite_master WHERE type='table'`;
    console.log('2. Tables:', tables.map(t => t.name));

    // 3. Try to insert a test row
    console.log('3. Inserting test row...');
    await sql`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;

    // 4. Read it back
    const rows = await sql`SELECT * FROM locations WHERE name = 'TEST'`;
    console.log('4. Test row:', rows);

    // 5. Count all rows
    const count = await sql`SELECT COUNT(*) as c FROM locations`;
    console.log('5. Total rows:', count[0].c);

    // 6. Delete test row
    await sql`DELETE FROM locations WHERE name = 'TEST'`;
    console.log('6. Test row deleted');

    console.log('=== TEST PASSED ===');
    return true;
  } catch (error) {
    console.error('=== TEST FAILED ===', error);
    return false;
}
}

// Expose to window for debugging
if (typeof window !== 'undefined') {
  window.testDatabase = testDatabase;
  window.dbStatus = getDatabaseStatus;
}

export async function closeDatabase() {
  channel.close();
  if (db.destroy) {
    await db.destroy();
}
}

// ============================================================================
// GPS Trails (storage adapter for the reusable GeoTracker module)
// ============================================================================
//
// These functions implement the GeoTracker "storage adapter" contract using
// SQLocal. They are intentionally generic (no map / UI coupling) so the same
// schema + helpers can be lifted into another app alongside src/geotracker/.

/**
 * Create a new trail row (status='recording') and return its local id.
 * @param {object} meta { uuid, name, startedAt, districtId }
 * @returns {Promise<number>} local trail id
 */
export async function createGpsTrail(meta) {
  const { uuid, name = null, startedAt, districtId = null } = meta;
  await sql`
    INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)
    VALUES (${uuid}, ${name}, ${districtId}, ${startedAt}, 'recording')
  `;
  const idResult = await sql`SELECT last_insert_rowid() as id`;
  const id = idResult[0]?.id;
  broadcastChange('gps_trails', 'insert', id);
  return id;
}

/**
 * Append one recorded point to a trail.
 * @param {number} trailId
 * @param {object} point normalized fix + { seq }
 */
export async function addGpsTrailPoint(trailId, point) {
  const {
    seq, lon, lat,
    altitude = null, accuracy = null, altitudeAccuracy = null,
    heading = null, speed = null, satellites = null, timestamp,
  } = point;
  const recordedAt = typeof timestamp === 'number' ? new Date(timestamp).toISOString() : (timestamp || new Date().toISOString());
  await sql`
    INSERT INTO gps_trail_points
      (trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)
    VALUES
      (${trailId}, ${seq}, ${lon}, ${lat}, ${altitude}, ${accuracy}, ${altitudeAccuracy}, ${heading}, ${speed}, ${satellites}, ${recordedAt})
  `;
}

/**
 * Finalise a trail: mark completed and store the summary.
 * @param {number} trailId
 * @param {object} summary { endedAt, pointCount, distanceM }
 */
export async function finishGpsTrail(trailId, summary) {
  const { endedAt, pointCount = 0, distanceM = 0 } = summary;
  await sql`
    UPDATE gps_trails
       SET ended_at = ${endedAt}, point_count = ${pointCount}, distance_m = ${distanceM}, status = 'completed'
     WHERE id = ${trailId}
  `;
  broadcastChange('gps_trails', 'update', trailId);
}

/**
 * Trails that are completed but not yet pushed to the server.
 * @returns {Promise<Array>}
 */
export async function getUnsyncedGpsTrails() {
  return sql`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`;
}

/**
 * All points of a trail, in recorded order.
 * @param {number} trailId
 * @returns {Promise<Array>}
 */
export async function getGpsTrailPoints(trailId) {
  return sql`SELECT * FROM gps_trail_points WHERE trail_id = ${trailId} ORDER BY seq ASC`;
}

/**
 * Mark a trail as synced and record the server-assigned id.
 * @param {number} trailId
 * @param {string|number|null} remoteId
 */
export async function markGpsTrailSynced(trailId, remoteId = null) {
  await sql`UPDATE gps_trails SET synced = 1, remote_id = ${remoteId} WHERE id = ${trailId}`;
  broadcastChange('gps_trails', 'update', trailId);
}

/**
 * List trails (most recent first) for a history/list UI.
 * @returns {Promise<Array>}
 */
export async function getGpsTrails() {
  return sql`SELECT * FROM gps_trails ORDER BY started_at DESC`;
}

/**
 * Delete a trail and all its points.
 * @param {number} trailId
 */
export async function deleteGpsTrail(trailId) {
  await sql`DELETE FROM gps_trail_points WHERE trail_id = ${trailId}`;
  await sql`DELETE FROM gps_trails WHERE id = ${trailId}`;
  broadcastChange('gps_trails', 'delete', trailId);
}

export default {
  sql,
  dbReady,
  initSchema,
  addLocation,
  getLocations,
  getLocation,
  updateLocation,
  deleteLocation,
  getLocationCount,
  getUnsyncedChanges,
  getUnsyncedLocations,
  markSynced,
  markLocationsSynced,
  saveRemoteData,
  getRemoteData,
  saveCollectorZones,
  getLocalCollectorZones,
  saveUpnGrid,
  getLocalUpnGrid,
  createExternalImport,
  addExternalImportFeatures,
  updateExternalImport,
  getExternalImport,
  listExternalImports,
  getExternalImportFeatures,
  remapImportedFeatureProperties,
  updateExternalImportFeatureGeometry,
  deleteExternalImportFeature,
  deleteExternalImport,
  saveParcels,
  getLocalParcels,
  updateParcel,
  insertNewParcel,
  saveBuildingFootprints,
  getLocalBuildingFootprints,
  saveOSMRoads,
  getLocalOSMRoads,
  createGpsTrail,
  addGpsTrailPoint,
  finishGpsTrail,
  getUnsyncedGpsTrails,
  getGpsTrailPoints,
  markGpsTrailSynced,
  getGpsTrails,
  deleteGpsTrail,
  CACHED_LAYER_TABLES,
  isCachedLayerTable,
  clearTable,
  clearAllCachedLayers,
  exportDatabase,
  exportToGeoJSON,
  importDatabase,
  downloadDatabase,
  getDatabaseStatus,
  getTableStats,
  getTableContent,
  testDatabase,
  onDatabaseChange,
  closeDatabase
};
