// Cloud sync — talks to dist/server/worker.js /sync endpoint.
// Strategy:
//   * Snapshot = full app state (accounts, categories, subs, credits, goals, incomeSources, settings, transactions, rates).
//   * Identified by SHA-256(email) so the server never sees raw email.
//   * updatedAt = milliseconds since epoch.
//   * Last-write-wins: client posts {updatedAt, snapshot}; server rejects with 409 if its copy is newer.
//   * Autosync: debounced 4s after any mutation; also runs every 60s; and once on app boot.

const Sync = {
  _userId: null,
  _serverUrl: null,
  _lastSyncedAt: 0,
  _pending: false,
  _timer: null,
  _listeners: new Set(),
  _status: 'idle',           // 'idle' | 'syncing' | 'ok' | 'error' | 'conflict' | 'disabled'
  _statusText: '',
  _txStoreGetter: null,
  _onRemoteApplied: null,

  configure({ email, serverUrl, txStoreGetter, onRemoteApplied }) {
    this._serverUrl = (serverUrl || '').replace(/\/$/, '') || null;
    this._txStoreGetter = txStoreGetter;
    this._onRemoteApplied = onRemoteApplied;
    if (email) {
      sha256Hex(email.trim().toLowerCase()).then(id => { this._userId = id; this._notify(); });
    } else {
      this._userId = null;
    }
    if (!this._serverUrl) {
      this._setStatus('disabled', 'URL не задан');
    }
  },

  onChange(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); },
  _notify() { for (const fn of this._listeners) try { fn(this.getStatus()); } catch {} },
  _setStatus(s, t) { this._status = s; this._statusText = t || ''; this._notify(); },
  getStatus() {
    return {
      enabled: !!(this._serverUrl && this._userId),
      status: this._status, text: this._statusText,
      lastSyncedAt: this._lastSyncedAt, userId: this._userId,
      serverUrl: this._serverUrl,
    };
  },

  // Snapshot of everything we want to persist remotely.
  buildSnapshot() {
    const txs = this._txStoreGetter ? this._txStoreGetter() : (window.TxStore ? TxStore.list() : []);
    let settings = {};
    try { settings = JSON.parse(localStorage.getItem('money:settings') || '{}'); } catch {}
    return {
      version: 1,
      accounts: DATA.accounts,
      categories: DATA.categories,
      subs: DATA.subs,
      credits: DATA.credits,
      goals: DATA.goals,
      incomeSources: DATA.incomeSources,
      rates: DATA.rates,
      // Exclude AI key from cloud — it stays local-only for security.
      settings: { ...settings, aiKey: '' },
      transactions: txs,
    };
  },

  // Apply a snapshot from the server, replacing local state.
  applySnapshot(snap) {
    if (!snap || typeof snap !== 'object') return;
    if (snap.accounts)      { DATA.accounts = snap.accounts; persistAccounts(); }
    if (snap.categories)    { DATA.categories = snap.categories; persistCategories(); }
    if (snap.subs)          { DATA.subs = snap.subs; persistSubs(); }
    if (snap.credits)       { DATA.credits = snap.credits; persistCredits(); }
    if (snap.goals)         { DATA.goals = snap.goals; persistGoals(); }
    if (snap.incomeSources) { DATA.incomeSources = snap.incomeSources; persistIncomeSources(); }
    if (snap.rates)         { DATA.rates = snap.rates; saveLS('money:rates', DATA.rates); }
    if (snap.settings) {
      try {
        const cur = JSON.parse(localStorage.getItem('money:settings') || '{}');
        // Preserve local-only AI key.
        const merged = { ...snap.settings, aiKey: cur.aiKey || '', pushSubscription: cur.pushSubscription || snap.settings.pushSubscription };
        localStorage.setItem('money:settings', JSON.stringify(merged));
      } catch {}
    }
    if (Array.isArray(snap.transactions) && window.TxStore) {
      TxStore.importAll(snap.transactions);
    }
    if (this._onRemoteApplied) try { this._onRemoteApplied(snap); } catch {}
  },

  // Pull latest from server. Resolves true if applied something.
  async pull() {
    if (!this._serverUrl || !this._userId) return false;
    this._setStatus('syncing', 'Загружаю с сервера…');
    try {
      const res = await fetch(this._serverUrl + '/sync?u=' + this._userId, { method: 'GET' });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const j = await res.json();
      if (j.empty) {
        this._setStatus('ok', 'Сервер пуст — буду заливать');
        return false;
      }
      if (!j.snapshot || !j.updatedAt) throw new Error('bad response');
      // If our local state is newer, skip apply.
      const localUpd = parseInt(localStorage.getItem('money:sync:updatedAt') || '0', 10);
      if (localUpd > j.updatedAt) {
        this._setStatus('ok', 'Локальные данные новее — синкаю на сервер');
        this.schedule(); // push our newer state
        return false;
      }
      this.applySnapshot(j.snapshot);
      this._lastSyncedAt = j.updatedAt;
      try { localStorage.setItem('money:sync:updatedAt', String(j.updatedAt)); } catch {}
      this._setStatus('ok', 'Синхронизировано');
      return true;
    } catch (e) {
      this._setStatus('error', e.message);
      return false;
    }
  },

  // Push current state to server.
  async push({ force = false } = {}) {
    if (!this._serverUrl || !this._userId) return false;
    this._setStatus('syncing', 'Сохраняю на сервер…');
    try {
      const updatedAt = Date.now();
      const snapshot = this.buildSnapshot();
      const res = await fetch(this._serverUrl + '/sync', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId: this._userId, updatedAt, snapshot, force }),
      });
      if (res.status === 409) {
        const j = await res.json().catch(() => ({}));
        if (j.server && j.server.snapshot) {
          // Server has newer data — apply it locally, then notify user.
          this.applySnapshot(j.server.snapshot);
          this._lastSyncedAt = j.server.updatedAt || 0;
          try { localStorage.setItem('money:sync:updatedAt', String(this._lastSyncedAt)); } catch {}
        }
        this._setStatus('conflict', 'На сервере была более новая версия — она применена');
        return false;
      }
      if (!res.ok) throw new Error('HTTP ' + res.status);
      this._lastSyncedAt = updatedAt;
      try { localStorage.setItem('money:sync:updatedAt', String(updatedAt)); } catch {}
      this._setStatus('ok', 'Синхронизировано');
      return true;
    } catch (e) {
      this._setStatus('error', e.message);
      return false;
    }
  },

  // Mark local state as dirty and schedule a debounced push.
  schedule(delay = 4000) {
    if (!this._serverUrl || !this._userId) return;
    if (this._timer) clearTimeout(this._timer);
    this._timer = setTimeout(() => { this._timer = null; this.push(); }, delay);
  },

  // Hard reset: wipe server copy.
  async reset() {
    if (!this._serverUrl || !this._userId) return false;
    try {
      const res = await fetch(this._serverUrl + '/sync/reset', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId: this._userId }),
      });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      try { localStorage.removeItem('money:sync:updatedAt'); } catch {}
      this._setStatus('ok', 'Сервер очищен');
      return true;
    } catch (e) {
      this._setStatus('error', e.message);
      return false;
    }
  },
};

async function sha256Hex(str) {
  const bytes = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
}

Object.assign(window, { Sync, sha256Hex });
