// ledger-data.jsx — categories, sample data, helpers

const CATEGORIES = [
  { id: 'salary',    name: 'Salary',     icon: '💷', color: 'oklch(0.55 0.10 150)', kind: 'in' },
  { id: 'side',      name: 'Side income',icon: '💼', color: 'oklch(0.58 0.10 175)', kind: 'in' },
  { id: 'refund',    name: 'Refund',     icon: '↩',  color: 'oklch(0.62 0.08 175)', kind: 'in' },

  { id: 'rent',      name: 'Rent',       icon: '🏠', color: 'oklch(0.55 0.08 280)', kind: 'out' },
  { id: 'bills',     name: 'Bills',      icon: '⚡', color: 'oklch(0.62 0.13 70)',  kind: 'out' },
  { id: 'groceries', name: 'Groceries',  icon: '🛒', color: 'oklch(0.60 0.12 130)', kind: 'out' },
  { id: 'food',      name: 'Food out',   icon: '🍜', color: 'oklch(0.62 0.13 38)',  kind: 'out' },
  { id: 'coffee',    name: 'Coffee',     icon: '☕', color: 'oklch(0.55 0.10 50)',  kind: 'out' },
  { id: 'transport', name: 'Transport',  icon: '🚌', color: 'oklch(0.55 0.09 220)', kind: 'out' },
  { id: 'fuel',      name: 'Fuel',       icon: '⛽', color: 'oklch(0.55 0.13 25)',  kind: 'out' },
  { id: 'fun',       name: 'Fun',        icon: '🎬', color: 'oklch(0.55 0.13 330)', kind: 'out' },
  { id: 'shopping',  name: 'Shopping',   icon: '🛍', color: 'oklch(0.58 0.11 5)',   kind: 'out' },
  { id: 'health',    name: 'Health',     icon: '⚕',  color: 'oklch(0.58 0.10 165)', kind: 'out' },
  { id: 'gifts',     name: 'Gifts',      icon: '🎁', color: 'oklch(0.58 0.11 350)', kind: 'out' },
  { id: 'other',     name: 'Other',      icon: '•',  color: 'oklch(0.60 0.01 60)',  kind: 'out' },
];
const CAT_MAP = Object.fromEntries(CATEGORIES.map(c => [c.id, c]));

// Sample transactions for May 2026, in reverse chronological-ish.
const SAMPLE_TX = [
  { id: 'tx21', date: '2026-05-21', type: 'out', amount: 12.40,  category: 'food',      desc: 'Pho Real · lunch with Em',     tags: ['lunch'] },
  { id: 'tx20', date: '2026-05-21', type: 'out', amount: 58.10,  category: 'fuel',      desc: 'Shell · A40',                  tags: ['car'], fuel: { litres: 38.2, odo: 42318 } },
  { id: 'tx19', date: '2026-05-20', type: 'in',  amount: 2840.00,category: 'salary',    desc: 'May salary',                   tags: ['monthly'], recurring: true },
  { id: 'tx18', date: '2026-05-19', type: 'out', amount: 84.20,  category: 'bills',     desc: 'Octopus · electricity',        tags: ['utility'], recurring: true },
  { id: 'tx17', date: '2026-05-19', type: 'out', amount:  9.99,  category: 'bills',     desc: 'Spotify',                      tags: ['sub'], recurring: true },
  { id: 'tx16', date: '2026-05-18', type: 'out', amount: 46.18,  category: 'groceries', desc: 'Tesco · weekly shop',          tags: [] },
  { id: 'tx15', date: '2026-05-18', type: 'out', amount: 22.00,  category: 'fun',       desc: 'Cineworld',                    tags: ['date'] },
  { id: 'tx14', date: '2026-05-17', type: 'out', amount:  3.85,  category: 'coffee',    desc: 'Pret',                         tags: [] },
  { id: 'tx13', date: '2026-05-17', type: 'out', amount: 14.50,  category: 'food',      desc: 'Wagamama',                     tags: ['lunch'] },
  { id: 'tx12', date: '2026-05-16', type: 'out', amount: 39.99,  category: 'shopping',  desc: 'Uniqlo · linen tee',           tags: [] },
  { id: 'tx11', date: '2026-05-15', type: 'out', amount: 1100.00,category: 'rent',      desc: 'May rent',                     tags: ['recurring'], recurring: true },
  { id: 'tx10', date: '2026-05-14', type: 'in',  amount: 18.99,  category: 'refund',    desc: 'Amazon return',                tags: [] },
  { id: 'tx09', date: '2026-05-14', type: 'out', amount:  8.20,  category: 'coffee',    desc: 'Blue Tokai',                   tags: [] },
  { id: 'tx08', date: '2026-05-13', type: 'out', amount: 32.40,  category: 'transport', desc: 'TfL · weekly cap',             tags: [] },
  { id: 'tx07', date: '2026-05-12', type: 'out', amount: 65.00,  category: 'gifts',     desc: 'Mum birthday flowers',         tags: [] },
  { id: 'tx06', date: '2026-05-11', type: 'out', amount: 28.00,  category: 'fun',       desc: 'Climbing day pass',            tags: [] },
  { id: 'tx05', date: '2026-05-10', type: 'out', amount: 12.40,  category: 'food',      desc: 'Brunch · Caravan',             tags: ['weekend'] },
  { id: 'tx04', date: '2026-05-08', type: 'out', amount: 54.40,  category: 'fuel',      desc: 'BP · M40',                     tags: ['car'], fuel: { litres: 36.0, odo: 42006 } },
  { id: 'tx03', date: '2026-05-05', type: 'in',  amount: 180.00, category: 'side',      desc: 'Freelance · logo',             tags: [] },
  { id: 'tx02', date: '2026-05-04', type: 'out', amount: 42.00,  category: 'health',    desc: 'Dentist co-pay',               tags: [] },
  { id: 'tx01', date: '2026-05-03', type: 'out', amount: 7.50,   category: 'transport', desc: 'Uber home',                    tags: [] },
];

const BUDGETS = [
  { category: 'rent',      amount: 1100 },
  { category: 'bills',     amount: 200 },
  { category: 'groceries', amount: 250 },
  { category: 'food',      amount: 200 },
  { category: 'coffee',    amount: 40 },
  { category: 'transport', amount: 80 },
  { category: 'fuel',      amount: 150 },
  { category: 'fun',       amount: 120 },
  { category: 'shopping',  amount: 100 },
  { category: 'health',    amount: 50 },
];

const RECURRING = [
  { id: 'r1', desc: 'May salary',     category: 'salary',  amount: 2840, cadence: 'monthly', nextDate: '2026-06-20' },
  { id: 'r2', desc: 'Rent · landlord',category: 'rent',    amount: 1100, cadence: 'monthly', nextDate: '2026-06-15' },
  { id: 'r3', desc: 'Octopus',        category: 'bills',   amount: 84,   cadence: 'monthly', nextDate: '2026-06-19' },
  { id: 'r4', desc: 'Spotify',        category: 'bills',   amount: 9.99, cadence: 'monthly', nextDate: '2026-06-19' },
  { id: 'r5', desc: 'Gym',            category: 'health',  amount: 32,   cadence: 'monthly', nextDate: '2026-06-01' },
];

// helpers
const fmt = (n, opts = {}) => {
  const { sign = false, currency = '£' } = opts;
  const abs = Math.abs(n);
  const s = abs.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  if (!sign) return currency + s;
  if (n < 0) return '−' + currency + s;
  if (n > 0) return '+' + currency + s;
  return currency + s;
};
const fmt0 = (n) => '£' + Math.round(Math.abs(n)).toLocaleString('en-GB');

// ─── storage layer ─────────────────────────────────────────
// Local mode → localStorage. Cloud mode (future) → swap to fetch('/api/…').
// Same shape so views never care which backend they're talking to.
const STORE = {
  load(key, def) {
    try { const v = localStorage.getItem('ledger.' + key); return v != null ? JSON.parse(v) : def; }
    catch { return def; }
  },
  save(key, val) {
    try { localStorage.setItem('ledger.' + key, JSON.stringify(val)); } catch {}
  },
  remove(key) {
    try { localStorage.removeItem('ledger.' + key); } catch {}
  },
  clearAll() {
    try {
      Object.keys(localStorage).filter(k => k.startsWith('ledger.')).forEach(k => localStorage.removeItem(k));
    } catch {}
  },
};

// ─── CSV export ─────────────────────────────────────────
function exportCSV(txs, filename = 'ledger.csv') {
  const head = ['date', 'type', 'category', 'description', 'amount_gbp', 'tags', 'recurring', 'fuel_litres', 'fuel_odometer_mi'];
  const esc = (v) => {
    const s = v == null ? '' : String(v);
    if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
  };
  const rows = [head.join(',')];
  txs.forEach(t => {
    rows.push([
      t.date, t.type, t.category, t.desc, t.amount.toFixed(2),
      (t.tags || []).join(';'),
      t.recurring || '',
      t.fuel?.litres ?? '',
      t.fuel?.odo ?? '',
    ].map(esc).join(','));
  });
  const blob = new Blob([rows.join('\n')], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a);
  a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0);
}

const monthOf = (iso) => iso.slice(0, 7);
const dateLabel = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
};
const dayOf = (iso) => Number(iso.slice(8, 10));

const totals = (txs) => {
  let income = 0, spend = 0;
  txs.forEach(t => { if (t.type === 'in') income += t.amount; else spend += t.amount; });
  return { income, spend, net: income - spend };
};

const spendByCategory = (txs) => {
  const m = {};
  txs.forEach(t => {
    if (t.type !== 'out') return;
    m[t.category] = (m[t.category] || 0) + t.amount;
  });
  return Object.entries(m)
    .map(([category, amount]) => ({ category, amount, cat: CAT_MAP[category] }))
    .sort((a, b) => b.amount - a.amount);
};

const sixMonthSeries = (txs, anchorMonth) => {
  // anchorMonth = '2026-05'
  const [yy, mm] = anchorMonth.split('-').map(Number);
  const series = [];
  for (let i = 5; i >= 0; i--) {
    let m = mm - i, y = yy;
    while (m <= 0) { m += 12; y -= 1; }
    const key = `${y}-${String(m).padStart(2, '0')}`;
    const monthTx = txs.filter(t => monthOf(t.date) === key);
    let income = 0, spend = 0;
    monthTx.forEach(t => { if (t.type === 'in') income += t.amount; else spend += t.amount; });
    // for past months without data, synthesize for the chart
    if (monthTx.length === 0 && key !== anchorMonth) {
      const seed = (y * 13 + m) % 9;
      income = 2700 + seed * 40;
      spend = 1200 + seed * 65;
    }
    series.push({ key, label: new Date(y, m - 1, 1).toLocaleDateString('en-GB', { month: 'short' }), income, spend });
  }
  return series;
};

// ─── auth helpers ─────────────────────────────────────────
// Two backends: local (PBKDF2 over localStorage) and cloud (Cloudflare API
// via window.CLOUD). The frontend uses the same shape from both — sign-in
// returns {username}, errors throw.
const AUTH = {
  // --- cloud branch (when window.CLOUD.mode === 'cloud') ---
  // delegated to ledger-cloud.js

  // --- local branch ---
  randomSalt() {
    const a = crypto.getRandomValues(new Uint8Array(16));
    return Array.from(a).map(b => b.toString(16).padStart(2, '0')).join('');
  },
  async hash(password, salt) {
    const enc = new TextEncoder();
    const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
    const bits = await crypto.subtle.deriveBits({
      name: 'PBKDF2', salt: enc.encode(salt), iterations: 100_000, hash: 'SHA-256',
    }, key, 256);
    return Array.from(new Uint8Array(bits)).map(b => b.toString(16).padStart(2, '0')).join('');
  },

  hasAccount() {
    // cloud: a saved token means the user has an account on the server
    if (window.CLOUD?.mode === 'cloud') return !!window.CLOUD.token;
    return !!STORE.load('account', null);
  },
  getAccount() {
    if (window.CLOUD?.mode === 'cloud') {
      // username last known on this device; the server is the source of truth
      return STORE.load('session', null);
    }
    return STORE.load('account', null);
  },

  async createAccount(username, password) {
    if (window.CLOUD?.mode === 'cloud') {
      const u = await window.CLOUD.auth.signup({ username, password });
      STORE.save('session', { username: u.username });
      return u;
    }
    const salt = this.randomSalt();
    const passwordHash = await this.hash(password, salt);
    const account = { username: username.trim(), salt, passwordHash, createdAt: Date.now() };
    STORE.save('account', account);
    STORE.save('session', { username: account.username });
    return account;
  },

  async verify(username, password) {
    if (window.CLOUD?.mode === 'cloud') {
      try {
        const u = await window.CLOUD.auth.login({ username, password });
        STORE.save('session', { username: u.username });
        return true;
      } catch { return false; }
    }
    const account = STORE.load('account', null);
    if (!account || account.username !== username.trim()) return false;
    const h = await this.hash(password, account.salt);
    return h === account.passwordHash;
  },

  async signOut() {
    if (window.CLOUD?.mode === 'cloud') {
      await window.CLOUD.auth.logout();
    }
    STORE.remove('session');
  },

  async changeUsername(currentPassword, newUsername) {
    if (window.CLOUD?.mode === 'cloud') {
      const r = await window.CLOUD.auth.changeUsername(currentPassword, newUsername);
      STORE.save('session', { username: r.username });
      return r;
    }
    const account = STORE.load('account', null);
    if (!account) throw new Error('No account');
    if (!newUsername.trim()) throw new Error('Username cannot be empty');
    const ok = await this.hash(currentPassword, account.salt) === account.passwordHash;
    if (!ok) throw new Error('Wrong password');
    account.username = newUsername.trim();
    STORE.save('account', account);
    STORE.save('session', { username: account.username });
    return account;
  },

  async changePassword(currentPassword, newPassword) {
    if (window.CLOUD?.mode === 'cloud') {
      return window.CLOUD.auth.changePassword(currentPassword, newPassword);
    }
    const account = STORE.load('account', null);
    if (!account) throw new Error('No account');
    if (newPassword.length < 4) throw new Error('Password must be 4+ chars');
    const ok = await this.hash(currentPassword, account.salt) === account.passwordHash;
    if (!ok) throw new Error('Wrong current password');
    const salt = this.randomSalt();
    const passwordHash = await this.hash(newPassword, salt);
    account.salt = salt;
    account.passwordHash = passwordHash;
    STORE.save('account', account);
    return account;
  },
};

Object.assign(window, {
  CATEGORIES, CAT_MAP, SAMPLE_TX, BUDGETS, RECURRING,
  fmt, fmt0, monthOf, dateLabel, dayOf, totals, spendByCategory, sixMonthSeries,
  STORE, exportCSV, AUTH,
});
