// Sync the local IndexedDB up to the Cloudflare Worker.
// All requests are authenticated with the user's session token.

import { getPendingSync, markSynced, setSetting } from './db.js';
import { Media } from './repo.js';
import { SYNC } from './config.js';
import { getSession } from './auth.js';
import { obLog } from './logger.js';

const STORES_TO_SYNC = ['groups', 'campaigns', 'posts', 'jobs', 'logs'];

function authHeaders(sessionToken) {
  return { 'X-Session-Token': sessionToken };
}

// Upload any local media blobs that haven't been pushed to R2 yet.
async function uploadPendingMedia(endpoint, sessionToken) {
  const pending = await Media.pendingUploads();
  let uploaded = 0;
  for (const m of pending) {
    if (m.r2Key) continue;
    const ext = (m.mimeType || '').split('/')[1] || 'bin';
    const key = `${m.postId || 'orphan'}/${m.id}.${ext}`;
    try {
      const res = await fetch(`${endpoint.replace(/\/$/, '')}/upload?key=${encodeURIComponent(key)}`, {
        method: 'POST',
        headers: {
          ...authHeaders(sessionToken),
          'Content-Type': m.mimeType || 'application/octet-stream',
        },
        body: m.blob,
      });
      if (!res.ok) continue;
      const json = await res.json();
      if (json.ok) {
        await Media.markUploaded(m.id, json.key);
        uploaded++;
      }
    } catch (e) {
      console.warn('[sync] media upload failed', m.id, e);
    }
  }
  return uploaded;
}

export async function runSync() {
  const { token: sessionToken } = await getSession();
  if (!sessionToken) return { skipped: true, reason: 'not-logged-in' };
  const endpoint = SYNC.endpoint;

  try {
    // 1) Push media blobs to R2 first.
    const mediaUploaded = await uploadPendingMedia(endpoint, sessionToken);

    // 2) Push DB rows.
    const pending = await getPendingSync();
    const totals = { media: mediaUploaded };
    for (const store of STORES_TO_SYNC) {
      let records = pending[store] || [];
      if (!records.length) { totals[store] = 0; continue; }

      if (store === 'posts') {
        records = await Promise.all(records.map(async (p) => {
          const mediaRows = await Media.listByPost(p.id);
          const keys = mediaRows.map((m) => m.r2Key).filter(Boolean);
          return { ...p, mediaUrls: keys };
        }));
      }

      await uploadBatch(endpoint, sessionToken, store, records);
      for (const r of records) await markSynced(store, r.id);
      totals[store] = records.length;
    }
    await setSetting('lastSyncAt', Date.now());
    const pushed = Object.values(totals).reduce((a, b) => a + b, 0);
    if (pushed > 0) obLog.event('sync_ok', { context: totals });
    return { skipped: false, totals };
  } catch (e) {
    obLog.error('sync_failed', { stack: e?.stack, context: { msg: String(e?.message || e) } });
    throw e;
  }
}

async function uploadBatch(endpoint, sessionToken, store, records) {
  try {
    const res = await fetch(`${endpoint.replace(/\/$/, '')}/sync`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...authHeaders(sessionToken) },
      body: JSON.stringify({ store, records }),
    });
    if (!res.ok) {
      let body = '';
      try { body = (await res.json()).error || ''; } catch {}
      const msg = `[sync] ${store} failed: ${res.status} ${body}`;
      console.warn(msg);
      throw new Error(msg);
    }
    return true;
  } catch (e) {
    console.warn('[sync] network error', e);
    throw e;
  }
}

export async function testConnection() {
  try {
    const res = await fetch(`${SYNC.endpoint}/health`);
    return { ok: res.ok, status: res.status };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}
