// IndexedDB-backed transaction store.
// Schema:
//   db `money`, object store `transactions` keyed by id
//   indexes: by `date`, `accountId`, `categoryId`, `type`
// Falls back to in-memory + localStorage when IndexedDB is unavailable.

const TX_DB = 'money';
const TX_STORE = 'transactions';
const TX_VERSION = 1;
const TX_LS_FALLBACK = 'money:txs:fallback';

function openDB() {
  return new Promise((resolve, reject) => {
    if (!window.indexedDB) return reject(new Error('IndexedDB unavailable'));
    const req = indexedDB.open(TX_DB, TX_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(TX_STORE)) {
        const os = db.createObjectStore(TX_STORE, { keyPath: 'id' });
        os.createIndex('date', 'date');
        os.createIndex('accountId', 'accountId');
        os.createIndex('categoryId', 'categoryId');
        os.createIndex('type', 'type');
      }
    };
    req.onerror = () => reject(req.error);
    req.onsuccess = () => resolve(req.result);
  });
}

const TxStore = {
  _cache: null,
  _ready: null,
  _listeners: new Set(),

  // Open DB once, load all txs into in-memory cache for fast reads.
  init() {
    if (this._ready) return this._ready;
    this._ready = (async () => {
      try {
        const db = await openDB();
        const tx = db.transaction(TX_STORE, 'readonly');
        const all = await reqPromise(tx.objectStore(TX_STORE).getAll());
        this._cache = Array.isArray(all) ? all : [];
        this._mode = 'idb';
      } catch (e) {
        const raw = localStorage.getItem(TX_LS_FALLBACK);
        this._cache = raw ? JSON.parse(raw) : [];
        this._mode = 'ls';
        console.warn('[store] IndexedDB unavailable, fallback to localStorage:', e.message);
      }
      this._cache.sort((a,b) => (b.date || '').localeCompare(a.date || ''));
      this._notify();
      return this;
    })();
    return this._ready;
  },

  onChange(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); },
  _notify() { for (const fn of this._listeners) try { fn(this._cache); } catch (e) { console.error(e); } },

  list() { return this._cache ? this._cache.slice() : []; },

  // Get with filters.
  query(opts = {}) {
    const all = this._cache || [];
    return all.filter(t => {
      if (opts.type && t.type !== opts.type) return false;
      if (opts.accountId && t.accountId !== opts.accountId) return false;
      if (opts.accountIds && !opts.accountIds.includes(t.accountId)) return false;
      if (opts.categoryId && t.categoryId !== opts.categoryId) return false;
      if (opts.categoryIds && !opts.categoryIds.includes(t.categoryId)) return false;
      if (opts.dateFrom && t.date < opts.dateFrom) return false;
      if (opts.dateTo && t.date > opts.dateTo) return false;
      if (opts.q) {
        const q = opts.q.toLowerCase();
        if (!(t.note || '').toLowerCase().includes(q)) return false;
      }
      return true;
    });
  },

  async _writeOne(tx) {
    if (this._mode === 'idb') {
      const db = await openDB();
      const t = db.transaction(TX_STORE, 'readwrite');
      t.objectStore(TX_STORE).put(tx);
      await transactionDone(t);
    } else {
      localStorage.setItem(TX_LS_FALLBACK, JSON.stringify(this._cache));
    }
  },
  async _deleteOne(id) {
    if (this._mode === 'idb') {
      const db = await openDB();
      const t = db.transaction(TX_STORE, 'readwrite');
      t.objectStore(TX_STORE).delete(id);
      await transactionDone(t);
    } else {
      localStorage.setItem(TX_LS_FALLBACK, JSON.stringify(this._cache));
    }
  },
  async _bulk(items) {
    if (this._mode === 'idb') {
      const db = await openDB();
      const t = db.transaction(TX_STORE, 'readwrite');
      const os = t.objectStore(TX_STORE);
      for (const it of items) os.put(it);
      await transactionDone(t);
    } else {
      localStorage.setItem(TX_LS_FALLBACK, JSON.stringify(this._cache));
    }
  },

  async add(tx) {
    const id = tx.id || ('tx_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7));
    const full = {
      id,
      type: tx.type || 'expense',                  // 'expense' | 'income' | 'transfer'
      amount: Math.abs(parseFloat(tx.amount) || 0),
      currency: tx.currency || 'RUB',
      currencyAmount: tx.currencyAmount,           // original-currency amount when foreign
      categoryId: tx.categoryId || 'other',
      accountId: tx.accountId,
      date: tx.date || new Date().toISOString(),
      note: tx.note || '',
      source: tx.source || 'manual',
      isPlanned: !!tx.isPlanned,
      recurringId: tx.recurringId || null,
    };
    this._cache.unshift(full);
    this._cache.sort((a,b) => (b.date || '').localeCompare(a.date || ''));
    await this._writeOne(full);
    applyTxToBalances(full, +1);
    this._notify();
    return full;
  },

  async update(id, patch) {
    const i = this._cache.findIndex(t => t.id === id);
    if (i < 0) return null;
    const old = this._cache[i];
    applyTxToBalances(old, -1);
    const next = { ...old, ...patch };
    this._cache[i] = next;
    this._cache.sort((a,b) => (b.date || '').localeCompare(a.date || ''));
    await this._writeOne(next);
    applyTxToBalances(next, +1);
    this._notify();
    return next;
  },

  async remove(id) {
    const i = this._cache.findIndex(t => t.id === id);
    if (i < 0) return false;
    const old = this._cache[i];
    applyTxToBalances(old, -1);
    this._cache.splice(i, 1);
    await this._deleteOne(id);
    this._notify();
    return true;
  },

  async clear() {
    for (const t of this._cache.slice()) applyTxToBalances(t, -1);
    this._cache = [];
    if (this._mode === 'idb') {
      const db = await openDB();
      const t = db.transaction(TX_STORE, 'readwrite');
      t.objectStore(TX_STORE).clear();
      await transactionDone(t);
    } else {
      localStorage.removeItem(TX_LS_FALLBACK);
    }
    this._notify();
  },

  async importAll(items) {
    if (!Array.isArray(items)) return;
    await this.clear();
    this._cache = items.slice();
    this._cache.sort((a,b) => (b.date || '').localeCompare(a.date || ''));
    await this._bulk(this._cache);
    for (const t of this._cache) applyTxToBalances(t, +1);
    this._notify();
  },

  // Derived aggregates — used by Home/Analytics/Calendar.
  sums(opts = {}) {
    let income = 0, expense = 0;
    for (const t of this.query(opts)) {
      if (t.type === 'income') income += t.amount;
      else if (t.type === 'expense') expense += t.amount;
    }
    return { income, expense, net: income - expense };
  },
  byCategory(opts = {}) {
    const m = {};
    for (const t of this.query({ ...opts, type: 'expense' })) {
      m[t.categoryId] = (m[t.categoryId] || 0) + t.amount;
    }
    return m;
  },
};

function reqPromise(req) {
  return new Promise((res, rej) => { req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); });
}
function transactionDone(tx) {
  return new Promise((res, rej) => {
    tx.oncomplete = () => res();
    tx.onerror = () => rej(tx.error);
    tx.onabort = () => rej(tx.error || new Error('abort'));
  });
}

// Apply (or revert) a transaction's effect on account balance and category spent.
function applyTxToBalances(tx, sign /* +1 add, -1 remove */) {
  if (!window.DATA) return;
  const acct = DATA.accounts.find(a => a.id === tx.accountId);
  if (acct) {
    if (tx.type === 'expense') acct.balance -= sign * tx.amount;
    else if (tx.type === 'income') acct.balance += sign * tx.amount;
    persistAccounts();
  }
  if (tx.type === 'expense' && tx.categoryId) {
    const c = DATA.categories.find(c => c.id === tx.categoryId);
    if (c) {
      c.spent = Math.max(0, (c.spent || 0) + sign * tx.amount);
    }
  }
  // Recompute monthly totals (current month only).
  if (typeof recomputeMonth === 'function') recomputeMonth();
}

function recomputeMonth() {
  const now = new Date();
  const monthKey = now.toISOString().slice(0, 7); // YYYY-MM
  let income = 0, expense = 0;
  for (const t of TxStore.list()) {
    if ((t.date || '').slice(0, 7) !== monthKey) continue;
    if (t.type === 'income')  income  += t.amount;
    if (t.type === 'expense') expense += t.amount;
  }
  DATA.income.actual  = income;
  DATA.expense.actual = expense;
}

Object.assign(window, { TxStore, recomputeMonth });
