// IndexedDB wrapper for FB Group Poster
// Designed so every record can be synced to a central DB later
// without refactoring: each row has clientId, updatedAt, syncStatus.

const DB_NAME = 'fb_poster_db';
const DB_VERSION = 4;

const STORES = {
  groups: 'groups',           // Facebook groups the user is member of OR discovered
  campaigns: 'campaigns',     // A campaign: name + list of groups + rotation policy
  posts: 'posts',             // Post templates — each belongs to a campaign
  media: 'media',             // Image/video Blobs attached to posts (uploaded to R2)
  jobs: 'jobs',               // Queued publish jobs (campaignId -> groupId; post chosen at runtime)
  logs: 'logs',               // History of every publish attempt
  settings: 'settings',       // Key/value config (clientId, delays, daily caps...)
  discoveries: 'discoveries', // Search queries + cached results
  comments: 'comments',       // Queued comments (bumps, replies, manual)
};

let _dbPromise = null;
let _clientId = null;

function openDB() {
  if (_dbPromise) return _dbPromise;
  _dbPromise = new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onerror = () => {
      // VersionError = existing DB is at a HIGHER version (e.g. user had a newer
      // build before, now back on an older one). Retry without specifying a
      // version — that opens whatever exists. This keeps the popup working
      // instead of throwing an unhandled-rejection that breaks subsequent code.
      if (req.error && req.error.name === 'VersionError') {
        const retry = indexedDB.open(DB_NAME);
        retry.onerror = () => reject(retry.error);
        retry.onsuccess = () => resolve(retry.result);
        return;
      }
      reject(req.error);
    };
    req.onsuccess = () => resolve(req.result);
    req.onupgradeneeded = (e) => {
      const db = e.target.result;

      if (!db.objectStoreNames.contains(STORES.groups)) {
        const s = db.createObjectStore(STORES.groups, { keyPath: 'id' });
        s.createIndex('fbGroupId', 'fbGroupId', { unique: false });
        s.createIndex('isMember', 'isMember', { unique: false });
        s.createIndex('source', 'source', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.posts)) {
        const s = db.createObjectStore(STORES.posts, { keyPath: 'id' });
        s.createIndex('updatedAt', 'updatedAt', { unique: false });
        s.createIndex('campaignId', 'campaignId', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      } else if (e.oldVersion < 2) {
        // v1 -> v2: posts existed without campaignId. Migration is in upgradeData() below.
        const s = e.target.transaction.objectStore(STORES.posts);
        if (!s.indexNames.contains('campaignId')) {
          s.createIndex('campaignId', 'campaignId', { unique: false });
        }
      }

      if (!db.objectStoreNames.contains(STORES.campaigns)) {
        const s = db.createObjectStore(STORES.campaigns, { keyPath: 'id' });
        s.createIndex('status', 'status', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.jobs)) {
        const s = db.createObjectStore(STORES.jobs, { keyPath: 'id' });
        s.createIndex('status', 'status', { unique: false });
        s.createIndex('scheduledAt', 'scheduledAt', { unique: false });
        s.createIndex('campaignId', 'campaignId', { unique: false });
        s.createIndex('groupId', 'groupId', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      } else if (e.oldVersion < 2) {
        const s = e.target.transaction.objectStore(STORES.jobs);
        if (!s.indexNames.contains('campaignId')) {
          s.createIndex('campaignId', 'campaignId', { unique: false });
        }
      }

      if (!db.objectStoreNames.contains(STORES.logs)) {
        const s = db.createObjectStore(STORES.logs, { keyPath: 'id' });
        s.createIndex('createdAt', 'createdAt', { unique: false });
        s.createIndex('jobId', 'jobId', { unique: false });
        s.createIndex('level', 'level', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.settings)) {
        db.createObjectStore(STORES.settings, { keyPath: 'key' });
      }

      if (!db.objectStoreNames.contains(STORES.media)) {
        const s = db.createObjectStore(STORES.media, { keyPath: 'id' });
        s.createIndex('postId', 'postId', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
        s.createIndex('r2Key', 'r2Key', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.comments)) {
        const s = db.createObjectStore(STORES.comments, { keyPath: 'id' });
        s.createIndex('status', 'status', { unique: false });
        s.createIndex('scheduledAt', 'scheduledAt', { unique: false });
        s.createIndex('parentPostUrl', 'parentPostUrl', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.discoveries)) {
        const s = db.createObjectStore(STORES.discoveries, { keyPath: 'id' });
        s.createIndex('query', 'query', { unique: false });
        s.createIndex('createdAt', 'createdAt', { unique: false });
      }
    };
  });
  return _dbPromise;
}

function tx(storeName, mode = 'readonly') {
  return openDB().then((db) => {
    const t = db.transaction(storeName, mode);
    return t.objectStore(storeName);
  });
}

function reqToPromise(req) {
  return new Promise((resolve, reject) => {
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

function uuid() {
  // crypto.randomUUID is available in service workers & modern browsers
  return (crypto.randomUUID && crypto.randomUUID()) ||
    'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      const r = (Math.random() * 16) | 0;
      const v = c === 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
    });
}

async function getClientId() {
  if (_clientId) return _clientId;
  const store = await tx(STORES.settings, 'readwrite');
  const existing = await reqToPromise(store.get('clientId'));
  if (existing) {
    _clientId = existing.value;
    return _clientId;
  }
  _clientId = uuid();
  await reqToPromise(store.put({ key: 'clientId', value: _clientId }));
  return _clientId;
}

// Stamp every record with sync metadata.
// `syncStatus`: 'pending' = needs upload to central DB, 'synced' = already there, 'local' = never sync.
async function stamp(record, { existing = false } = {}) {
  const clientId = await getClientId();
  const now = Date.now();
  return {
    ...record,
    id: record.id || uuid(),
    clientId,
    updatedAt: now,
    createdAt: existing ? record.createdAt : (record.createdAt || now),
    syncStatus: 'pending',
  };
}

// ---------- Generic CRUD ----------

export async function put(storeName, record) {
  const stamped = await stamp(record, { existing: !!record.createdAt });
  const store = await tx(storeName, 'readwrite');
  await reqToPromise(store.put(stamped));
  return stamped;
}

export async function get(storeName, id) {
  const store = await tx(storeName);
  return reqToPromise(store.get(id));
}

export async function getAll(storeName) {
  const store = await tx(storeName);
  return reqToPromise(store.getAll());
}

export async function getByIndex(storeName, indexName, value) {
  const store = await tx(storeName);
  const idx = store.index(indexName);
  return reqToPromise(idx.getAll(value));
}

export async function remove(storeName, id) {
  const store = await tx(storeName, 'readwrite');
  return reqToPromise(store.delete(id));
}

export async function clearStore(storeName) {
  const store = await tx(storeName, 'readwrite');
  return reqToPromise(store.clear());
}

// ---------- Settings helpers (simple K/V) ----------

export async function setSetting(key, value) {
  const store = await tx(STORES.settings, 'readwrite');
  return reqToPromise(store.put({ key, value }));
}

export async function getSetting(key, fallback = null) {
  const store = await tx(STORES.settings);
  const row = await reqToPromise(store.get(key));
  return row ? row.value : fallback;
}

// ---------- Sync hook (for future central DB) ----------
// Returns all records across stores that still need to be pushed upstream.
export async function getPendingSync() {
  const result = {};
  for (const name of [STORES.groups, STORES.campaigns, STORES.posts, STORES.jobs, STORES.logs, STORES.comments]) {
    result[name] = await getByIndex(name, 'syncStatus', 'pending');
  }
  return result;
}

export async function markSynced(storeName, id) {
  const record = await get(storeName, id);
  if (!record) return;
  record.syncStatus = 'synced';
  const store = await tx(storeName, 'readwrite');
  await reqToPromise(store.put(record));
}

export { STORES, uuid, getClientId };
