// sync.jsx — file sync system
// - Manual export/import as JSON download/upload
// - Auto-sync to a linked file via File System Access API
// - Handle persists across reloads via IndexedDB
// Ports the behavior of the original workshop.html.

const SYNC_DB = 'workshop_v2_sync';
const SYNC_STORE = 'handles';
const SYNC_KEY = 'syncFile';

/* ---------- IndexedDB plumbing ---------- */

function openSyncDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(SYNC_DB, 1);
    req.onupgradeneeded = () => { req.result.createObjectStore(SYNC_STORE); };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function dbPutHandle(handle) {
  const db = await openSyncDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(SYNC_STORE, 'readwrite');
    tx.objectStore(SYNC_STORE).put(handle, SYNC_KEY);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

async function dbGetHandle() {
  try {
    const db = await openSyncDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(SYNC_STORE, 'readonly');
      const req = tx.objectStore(SYNC_STORE).get(SYNC_KEY);
      req.onsuccess = () => resolve(req.result || null);
      req.onerror = () => reject(req.error);
    });
  } catch (e) { return null; }
}

async function dbDeleteHandle() {
  try {
    const db = await openSyncDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(SYNC_STORE, 'readwrite');
      tx.objectStore(SYNC_STORE).delete(SYNC_KEY);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch (e) {}
}

/* ---------- payload + filename helpers ---------- */

function buildPayload(data) {
  return {
    app: 'workshop',
    version: 2,
    exportedAt: new Date().toISOString(),
    projects: data.projects,
    tasks: data.tasks,
    people: data.people || []
  };
}
function todayISO() {
  const d = new Date();
  return d.toISOString().slice(0, 10);
}
function fmtRelative(ts) {
  if (!ts) return '';
  const s = Math.floor((Date.now() - ts) / 1000);
  if (s < 5) return 'just now';
  if (s < 60) return s + 's ago';
  if (s < 3600) return Math.floor(s/60) + 'm ago';
  if (s < 86400) return Math.floor(s/3600) + 'h ago';
  return Math.floor(s/86400) + 'd ago';
}

/* ---------- toast system ---------- */
// Tiny self-contained toast. Renders to body, no external state.
// action (optional): { label: string, onClick: () => void }
function showToast(msg, kind, action) {
  const root = document.getElementById('sync-toast-root') || (function(){
    const r = document.createElement('div');
    r.id = 'sync-toast-root';
    r.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
    document.body.appendChild(r);
    return r;
  })();
  const el = document.createElement('div');
  const colors = {
    success: 'background:var(--paper-2);color:var(--ink);border-left:3px solid var(--green);',
    error: 'background:var(--paper-2);color:var(--ink);border-left:3px solid var(--red);',
    info: 'background:var(--paper-2);color:var(--ink);border-left:3px solid var(--ink-3);',
  };
  el.style.cssText = (colors[kind] || colors.info) +
    'padding:10px 16px;border-radius:6px;font-family:var(--hand);font-size:16px;' +
    'box-shadow:0 6px 24px rgba(0,0,0,0.18);pointer-events:auto;opacity:0;transform:translateY(8px);' +
    'transition:opacity 0.2s, transform 0.2s; max-width:340px;display:flex;align-items:center;gap:12px;';

  const msgSpan = document.createElement('span');
  msgSpan.textContent = msg;
  msgSpan.style.cssText = 'flex:1;';
  el.appendChild(msgSpan);

  let dismissTimer = null;
  const dismiss = () => {
    if (dismissTimer) { clearTimeout(dismissTimer); dismissTimer = null; }
    el.style.opacity = '0';
    el.style.transform = 'translateY(8px)';
    setTimeout(() => el.remove(), 250);
  };

  if (action && typeof action.onClick === 'function') {
    const btn = document.createElement('button');
    btn.textContent = action.label || 'undo';
    btn.style.cssText = 'background:transparent;border:1px solid var(--ink-4);color:var(--ink-2);' +
      'font-family:var(--mono);font-size:9px;letter-spacing:0.12em;text-transform:uppercase;' +
      'padding:4px 8px;border-radius:999px;cursor:pointer;flex:none;';
    btn.addEventListener('click', () => { action.onClick(); dismiss(); });
    el.appendChild(btn);
  }

  root.appendChild(el);
  requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; });
  dismissTimer = setTimeout(dismiss, 2800);
}

/* ---------- main hook ---------- */

function useSyncManager(data, onImport) {
  // status: 'unlinked' | 'synced' | 'paused' | 'syncing'
  const [status, setStatus] = React.useState('unlinked');
  const [filename, setFilename] = React.useState(null);
  const [lastSyncAt, setLastSyncAt] = React.useState(null);
  const handleRef = React.useRef(null);
  const writeTimer = React.useRef(null);
  const lastSerialized = React.useRef(null);

  // Load handle on mount
  React.useEffect(() => {
    (async () => {
      const h = await dbGetHandle();
      if (h) {
        handleRef.current = h;
        setFilename(h.name);
        // We can't auto-write without permission; show as paused until first user action
        try {
          const perm = await h.queryPermission({ mode: 'readwrite' });
          if (perm === 'granted') {
            setStatus('synced');
          } else {
            setStatus('paused');
          }
        } catch (e) { setStatus('paused'); }
      }
    })();
  }, []);

  // Watch data. Debounced write.
  React.useEffect(() => {
    if (!data) return;
    if (!handleRef.current) return;
    const serialized = JSON.stringify(buildPayload(data));
    if (serialized === lastSerialized.current) return;
    lastSerialized.current = serialized;

    if (writeTimer.current) clearTimeout(writeTimer.current);
    writeTimer.current = setTimeout(async () => {
      const ok = await writeToHandle(handleRef.current, serialized);
      if (ok) {
        setStatus('synced');
        setLastSyncAt(Date.now());
      } else {
        setStatus('paused');
      }
    }, 600);
    return () => { if (writeTimer.current) clearTimeout(writeTimer.current); };
  }, [data]);

  /* ---- actions ---- */

  const exportJSON = React.useCallback(() => {
    if (!data) return;
    const blob = new Blob([JSON.stringify(buildPayload(data), null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'workshop_backup_' + todayISO() + '.json';
    document.body.appendChild(a);
    a.click();
    setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 100);
    showToast('Exported backup', 'success');
  }, [data]);

  const importJSON = React.useCallback((file) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const obj = JSON.parse(e.target.result);
        if (!obj.projects || !obj.tasks) throw new Error('Invalid file: missing projects or tasks');
        const ok = window.confirm(
          `Replace current data?\n\n` +
          `Importing will REPLACE all current projects and tasks ` +
          `(${(data?.projects||[]).length} projects, ${(data?.tasks||[]).length} tasks).\n\n` +
          `You will get ${obj.projects.length} projects and ${obj.tasks.length} tasks.\n\n` +
          `This cannot be undone.`
        );
        if (!ok) return;
        // older backups have no people roster — keep the current one in that case
        onImport({ projects: obj.projects, tasks: obj.tasks, people: obj.people || data?.people || [] });
        showToast(`Imported ${obj.tasks.length} tasks`, 'success');
      } catch (err) {
        showToast('Import failed: ' + err.message, 'error');
      }
    };
    reader.readAsText(file);
  }, [data, onImport]);

  const linkSyncFile = React.useCallback(async () => {
    if (!window.showSaveFilePicker) {
      showToast('File sync requires Chrome, Edge, or Opera', 'error');
      return;
    }
    try {
      const handle = await window.showSaveFilePicker({
        suggestedName: 'workshop_data.json',
        types: [{ description: 'JSON file', accept: { 'application/json': ['.json'] } }]
      });
      handleRef.current = handle;
      setFilename(handle.name);
      await dbPutHandle(handle);
      // Write immediately
      const serialized = JSON.stringify(buildPayload(data));
      const ok = await writeToHandle(handle, serialized);
      if (ok) {
        setStatus('synced');
        setLastSyncAt(Date.now());
        lastSerialized.current = serialized;
        showToast('Linked ' + handle.name, 'success');
      } else {
        setStatus('paused');
      }
    } catch (e) {
      if (e.name !== 'AbortError') {
        showToast('Could not link file: ' + e.message, 'error');
      }
    }
  }, [data]);

  const unlinkSyncFile = React.useCallback(async () => {
    handleRef.current = null;
    setFilename(null);
    setStatus('unlinked');
    setLastSyncAt(null);
    await dbDeleteHandle();
    showToast('Unlinked sync file', 'info');
  }, []);

  // Resume: re-request permission and write current state
  const resumeSync = React.useCallback(async () => {
    if (!handleRef.current) return;
    try {
      const perm = await handleRef.current.requestPermission({ mode: 'readwrite' });
      if (perm !== 'granted') {
        showToast('Permission denied', 'error');
        return;
      }
      const serialized = JSON.stringify(buildPayload(data));
      const ok = await writeToHandle(handleRef.current, serialized);
      if (ok) {
        setStatus('synced');
        setLastSyncAt(Date.now());
        lastSerialized.current = serialized;
        showToast('Sync resumed', 'success');
      }
    } catch (e) {
      showToast('Could not resume: ' + e.message, 'error');
    }
  }, [data]);

  return {
    status, filename, lastSyncAt,
    exportJSON, importJSON, linkSyncFile, unlinkSyncFile, resumeSync,
    supported: !!window.showSaveFilePicker
  };
}

async function writeToHandle(handle, content) {
  if (!handle) return false;
  try {
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
    return true;
  } catch (e) {
    return false;
  }
}

/* ---------- UI: SyncSettings component ---------- */

function SyncSettings({ syncMgr }) {
  const [, force] = React.useReducer(x => x + 1, 0);
  const fileInputRef = React.useRef(null);

  // refresh "x ago" label every 30s
  React.useEffect(() => {
    const t = setInterval(force, 30000);
    return () => clearInterval(t);
  }, []);

  const { status, filename, lastSyncAt, exportJSON, importJSON, linkSyncFile, unlinkSyncFile, resumeSync, supported } = syncMgr;

  const handleFile = (e) => {
    const f = e.target.files[0];
    if (f) importJSON(f);
    e.target.value = ''; // reset so re-selecting same file fires
  };

  return (
    <div className="sync-settings">
      <div className="sync-menu-section">
        <div className="sync-menu-heading">Manual Backup</div>
        <button className="sync-menu-item" onClick={() => exportJSON()}>
          <span className="sync-menu-glyph">↓</span>
          <span>Export JSON…</span>
          <span className="sync-menu-hint">download backup</span>
        </button>
        <button className="sync-menu-item" onClick={() => fileInputRef.current?.click()}>
          <span className="sync-menu-glyph">↑</span>
          <span>Import JSON…</span>
          <span className="sync-menu-hint">replace data from file</span>
        </button>
      </div>

      <div className="sync-menu-divider"/>

      <div className="sync-menu-section">
        <div className="sync-menu-heading">
          Auto-sync to file
          {!supported && <span className="sync-menu-warn"> · unsupported in this browser</span>}
        </div>

        {status === 'unlinked' && (
          <button className="sync-menu-item" disabled={!supported} onClick={linkSyncFile}>
            <span className="sync-menu-glyph">⇆</span>
            <span>Link sync file…</span>
            <span className="sync-menu-hint">writes on every change</span>
          </button>
        )}

        {status === 'synced' && (
          <>
            <div className="sync-menu-status">
              <span className="sync-menu-glyph good">●</span>
              <div>
                <div className="sync-menu-filename">{filename}</div>
                <div className="sync-menu-substatus">
                  {lastSyncAt ? 'last write ' + fmtRelative(lastSyncAt) : 'live'}
                </div>
              </div>
            </div>
            <button className="sync-menu-item subtle" onClick={unlinkSyncFile}>
              <span className="sync-menu-glyph">×</span>
              <span>Unlink file</span>
            </button>
          </>
        )}

        {status === 'paused' && (
          <>
            <div className="sync-menu-status paused">
              <span className="sync-menu-glyph warn">⚠</span>
              <div>
                <div className="sync-menu-filename">{filename}</div>
                <div className="sync-menu-substatus">paused — needs permission</div>
              </div>
            </div>
            <button className="sync-menu-item" onClick={resumeSync}>
              <span className="sync-menu-glyph">↻</span>
              <span>Resume sync</span>
            </button>
            <button className="sync-menu-item subtle" onClick={unlinkSyncFile}>
              <span className="sync-menu-glyph">×</span>
              <span>Unlink file</span>
            </button>
          </>
        )}
      </div>

      <input ref={fileInputRef} type="file" accept="application/json"
        style={{display:'none'}} onChange={handleFile}/>
    </div>
  );
}

Object.assign(window, { useSyncManager, SyncSettings });
