#!/usr/bin/env node
/**
 * Seed Demo Data — a full "take it for a ride" dataset.
 *
 * Populates every user-facing surface (bills, categories + groups, payment
 * history with drift + debt paydown, monthly overrides/skips/snoozes, planning
 * income/starting amounts, spending budgets/rules/transactions, a demo bank
 * source + accounts + matched/unmatched/subscription transactions, an active
 * snowball plan, merchant rules, a calendar feed) so a new user can explore the
 * whole app.
 *
 * Removal contract: everything created here is either marked `is_seeded = 1` on a
 * user-scoped table, or is a bill-child that cascade-deletes with its seeded bill.
 * `POST /user/clear-demo-data` removes exactly these rows and nothing else — see
 * routes/user.cts. Keep that in sync when adding a new seeded surface.
 *
 * Idempotent: refuses to run when the account already has seeded bills.
 */

const path = require('path');

// DB_PATH is read at require-time by db/database — keep the default in sync.
process.env.DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'db', 'bills.db');

const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
// Money columns are integer cents (migration v1.03) — convert demo dollars.
const { toCents } = require('../utils/money.mts');

// ── Static demo content ─────────────────────────────────────────────────────

const BILL_CATEGORIES = [
  'Utilities',
  'Housing',
  'Insurance',
  'Subscriptions',
  'Transportation',
  'Healthcare',
  'Credit Cards',
  'Loans',
  'Entertainment',
];

// Spending categories are distinct from bill categories and from the app's
// DEFAULT_CATEGORIES, so they are always seed-created (and thus removable).
const SPENDING_CATEGORIES = ['Groceries', 'Dining Out', 'Gas & Fuel', 'Shopping', 'Coffee'];

const CATEGORY_GROUPS = [
  { name: 'Living Expenses', members: ['Groceries', 'Gas & Fuel'] },
  { name: 'Discretionary', members: ['Dining Out', 'Shopping', 'Coffee'] },
];

// billing_cycle: 'monthly' | 'quarterly' | 'annually' | 'irregular'
// cycle_type:    'monthly' | 'weekly' | 'biweekly' | 'quarterly' | 'annual'
const BILLS = [
  // ── Utilities ──
  {
    name: 'Electric Company',
    category: 'Utilities',
    amount: 85,
    dueDay: 15,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    autopayVerified: true,
  },
  {
    name: 'Gas Utility',
    category: 'Utilities',
    amount: 35,
    dueDay: 12,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
  },
  {
    name: 'City Water Dept',
    category: 'Utilities',
    amount: 45,
    dueDay: 20,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
  },
  {
    name: 'Internet Provider',
    category: 'Utilities',
    amount: 70,
    dueDay: 18,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    autopayVerified: true,
  },
  {
    name: 'Cell Phone',
    category: 'Utilities',
    amount: 65,
    dueDay: 25,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    currentBalance: 648,
    minPayment: 27,
    interestRate: 0,
    snowballOrder: 0,
    snowballInclude: 1,
  },
  {
    name: 'Trash Service',
    category: 'Utilities',
    amount: 25,
    dueDay: 28,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: false,
  },

  // ── Housing ──
  {
    name: 'Mortgage',
    category: 'Housing',
    amount: 1450,
    dueDay: 1,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    autopayVerified: true,
    interestRate: 3.25,
    currentBalance: 185000,
    minPayment: 1200,
    snowballInclude: 0,
    snowballExempt: 1,
  },

  // ── Insurance / Healthcare ──
  {
    name: 'Car Insurance',
    category: 'Insurance',
    amount: 120,
    dueDay: 5,
    cycle: 'quarterly',
    cycleType: 'quarterly',
    autopay: true,
  },
  {
    name: 'Homeowners Insurance',
    category: 'Insurance',
    amount: 300,
    dueDay: 10,
    cycle: 'annually',
    cycleType: 'annual',
    autopay: false,
  },
  {
    name: 'Health Insurance',
    category: 'Healthcare',
    amount: 200,
    dueDay: 1,
    cycle: 'quarterly',
    cycleType: 'quarterly',
    autopay: true,
  },
  {
    name: 'Dental Insurance',
    category: 'Healthcare',
    amount: 40,
    dueDay: 15,
    cycle: 'quarterly',
    cycleType: 'quarterly',
    autopay: true,
  },

  // ── Credit Cards (snowball, smallest→largest) ──
  {
    name: 'Discover It Card',
    category: 'Credit Cards',
    amount: 65,
    dueDay: 26,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    interestRate: 22.99,
    currentBalance: 920,
    minPayment: 35,
    snowballOrder: 1,
    snowballInclude: 1,
    website: 'https://discover.com',
    has2fa: true,
  },
  {
    name: 'Capital One Quicksilver',
    category: 'Credit Cards',
    amount: 95,
    dueDay: 28,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    interestRate: 24.49,
    currentBalance: 1850,
    minPayment: 55,
    snowballOrder: 2,
    snowballInclude: 1,
    website: 'https://capitalone.com',
    has2fa: true,
  },
  {
    name: 'Chase Freedom',
    category: 'Credit Cards',
    amount: 140,
    dueDay: 12,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    interestRate: 21.49,
    currentBalance: 3200,
    minPayment: 90,
    snowballOrder: 3,
    snowballInclude: 1,
    website: 'https://chase.com',
    has2fa: true,
  },

  // ── Loans ──
  {
    name: 'Car Payment',
    category: 'Loans',
    amount: 425,
    dueDay: 22,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    interestRate: 4.5,
    currentBalance: 8400,
    minPayment: 350,
    snowballOrder: 4,
    snowballInclude: 1,
  },
  {
    name: 'Student Loan',
    category: 'Loans',
    amount: 250,
    dueDay: 15,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    interestRate: 5.5,
    currentBalance: 12500,
    minPayment: 150,
    snowballOrder: 5,
    snowballInclude: 1,
  },

  // ── Subscriptions ──
  {
    name: 'Netflix',
    category: 'Subscriptions',
    amount: 15.99,
    dueDay: 22,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'streaming',
    website: 'https://netflix.com',
  },
  {
    name: 'Spotify',
    category: 'Subscriptions',
    amount: 9.99,
    dueDay: 14,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'music',
    website: 'https://spotify.com',
  },
  {
    name: 'Adobe Creative Cloud',
    category: 'Subscriptions',
    amount: 54.99,
    dueDay: 8,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'software',
    website: 'https://adobe.com',
    has2fa: true,
  },
  {
    name: 'Amazon Prime',
    category: 'Subscriptions',
    amount: 139,
    dueDay: 1,
    cycle: 'annually',
    cycleType: 'annual',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'shopping',
    website: 'https://amazon.com',
  },
  {
    name: 'Apple iCloud+',
    category: 'Subscriptions',
    amount: 2.99,
    dueDay: 18,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'cloud',
    website: 'https://icloud.com',
  },
  {
    name: 'Gym Membership',
    category: 'Subscriptions',
    amount: 45,
    dueDay: 10,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'fitness',
  },

  // ── Entertainment (a genuine recurring entertainment bill, monthly) ──
  {
    name: 'AMC A-List',
    category: 'Entertainment',
    amount: 24.99,
    dueDay: 3,
    cycle: 'monthly',
    cycleType: 'monthly',
    autopay: true,
    isSubscription: true,
    subscriptionType: 'entertainment',
    website: 'https://amctheatres.com',
  },
];

// Bills deliberately left OVERDUE this month (due date passed, no payment).
const OVERDUE_THIS_MONTH = new Set(['Trash Service', 'Dental Insurance']);
// Bills whose most-recent payments also appear as matched bank transactions.
const BANK_MATCHED = new Set([
  'Electric Company',
  'Internet Provider',
  'Netflix',
  'Spotify',
  'Car Payment',
]);
// One skipped month (bill leaves that month unpaid, flagged skipped not overdue).
const SKIP = { bill: 'Gym Membership', monthsAgo: 1 };
// One amount override (actual differs from expected) and one note.
const OVERRIDE = { bill: 'Electric Company', monthsAgo: 1, actual: 132.4 };
const NOTE = {
  bill: 'City Water Dept',
  text: 'Called about the leak — credit expected next cycle.',
};

// Un-tracked subscription charges (in the catalog, no matching bill) → drive the
// Subscriptions "recommendations" surface.
const UNTRACKED_SUBS = [
  { payee: 'HULU', amount: 17.99, day: 6 },
  { payee: 'HBO MAX', amount: 15.99, day: 11 },
  { payee: 'YOUTUBE PREMIUM', amount: 13.99, day: 19 },
];

// Everyday spending (not bills) → populate the Spending page + budgets/rules.
const SPENDING_MERCHANTS = [
  { payee: 'WHOLE FOODS MARKET', category: 'Groceries', amounts: [82.14, 46.03, 118.77] },
  { payee: "TRADER JOE'S", category: 'Groceries', amounts: [54.2, 61.88] },
  { payee: 'CHIPOTLE', category: 'Dining Out', amounts: [14.85, 22.4] },
  { payee: 'OLIVE GARDEN', category: 'Dining Out', amounts: [63.12] },
  { payee: 'SHELL OIL', category: 'Gas & Fuel', amounts: [48.9, 51.35] },
  { payee: 'AMAZON MARKETPLACE', category: 'Shopping', amounts: [39.99, 27.49, 112.0] },
  { payee: 'STARBUCKS', category: 'Coffee', amounts: [6.45, 5.75, 6.45] },
];
const SPENDING_RULES = [
  { merchant: 'WHOLE FOODS', category: 'Groceries' },
  { merchant: 'SHELL', category: 'Gas & Fuel' },
  { merchant: 'STARBUCKS', category: 'Coffee' },
  { merchant: 'AMAZON', category: 'Shopping' },
];
const SPENDING_BUDGETS = {
  Groceries: 500,
  'Dining Out': 200,
  'Gas & Fuel': 160,
  Shopping: 250,
  Coffee: 40,
};

const HISTORY_MONTHS = 6; // months of payment history to generate (incl. current)

// ── Date helpers ────────────────────────────────────────────────────────────

function ymd(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function clampedDate(year, monthIdx0, day) {
  const lastDay = new Date(year, monthIdx0 + 1, 0).getDate();
  return new Date(year, monthIdx0, Math.min(day, lastDay));
}
// Month offsets (0 = current, negative = past) a bill occurs in, within the window.
function cycleOffsets(bill) {
  switch (bill.cycle) {
    case 'annually':
      return [-1];
    case 'quarterly':
      return [0, -3];
    case 'irregular':
      return [0, -1, -2];
    default:
      return Array.from({ length: HISTORY_MONTHS }, (_, i) => -(HISTORY_MONTHS - 1) + i); // -5..0
  }
}
// Small deterministic-ish drift on variable bills so amounts differ from expected.
function driftedAmount(bill, offset) {
  const variable = ['Utilities'].includes(bill.category) && !bill.currentBalance;
  if (!variable) return bill.amount;
  const pct = ((Math.abs(bill.name.length * 7 + offset * 13) % 21) - 10) / 100; // ±10%
  return Math.round(bill.amount * (1 + pct) * 100) / 100;
}

// ── Main ────────────────────────────────────────────────────────────────────

function seedDemoData(userId = null) {
  const db = getDb();
  const now = new Date();
  const todayStr = ymd(now);

  const targetUser =
    userId !== null
      ? db.prepare('SELECT id FROM users WHERE id = ?').get(userId)
      : db.prepare("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1").get();
  if (!targetUser) throw new Error('User not found. Please create a user first.');
  const uid = targetUser.id;

  const alreadySeeded = db
    .prepare('SELECT COUNT(*) AS c FROM bills WHERE user_id = ? AND is_seeded = 1')
    .get(uid).c;
  if (alreadySeeded > 0) {
    const err = new Error(
      'Demo data is already seeded for this account. Clear it first to re-seed.',
    );
    err.status = 409;
    err.code = 'ALREADY_SEEDED';
    throw err;
  }

  const counts = {
    bills: 0,
    categories: 0,
    groups: 0,
    payments: 0,
    transactions: 0,
    monthlyState: 0,
    planning: 0,
    budgets: 0,
    spendingRules: 0,
    merchantRules: 0,
    snowballPlans: 0,
    calendarTokens: 0,
  };

  db.transaction(() => {
    ensureUserDefaultCategories(uid);

    // ── Categories (bill + spending), flagging ONLY ones this seed creates ──
    const catId = {};
    const getOrCreateCategory = (name, { spending = false } = {}) => {
      const existing = db
        .prepare('SELECT id FROM categories WHERE user_id = ? AND name = ? COLLATE NOCASE')
        .get(uid, name);
      if (existing) {
        catId[name] = existing.id;
        return;
      }
      const r = db
        .prepare(
          'INSERT INTO categories (user_id, name, spending_enabled, is_seeded) VALUES (?, ?, ?, 1)',
        )
        .run(uid, name, spending ? 1 : 0);
      catId[name] = r.lastInsertRowid;
      counts.categories++;
    };
    for (const name of BILL_CATEGORIES) getOrCreateCategory(name);
    for (const name of SPENDING_CATEGORIES) getOrCreateCategory(name, { spending: true });

    // ── Category groups (organise spending categories) ──
    for (const [i, grp] of CATEGORY_GROUPS.entries()) {
      const r = db
        .prepare(
          'INSERT INTO category_groups (user_id, name, sort_order, is_seeded) VALUES (?, ?, ?, 1)',
        )
        .run(uid, grp.name, i);
      counts.groups++;
      for (const member of grp.members) {
        if (catId[member])
          db.prepare('UPDATE categories SET group_id = ? WHERE id = ?').run(
            r.lastInsertRowid,
            catId[member],
          );
      }
    }

    // ── Bills ──
    const insertBill = db.prepare(`
      INSERT INTO bills (
        user_id, name, category_id, due_day, billing_cycle, cycle_type,
        expected_amount, autopay_enabled, autodraft_status, autopay_verified_at, interest_rate,
        current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt,
        is_subscription, subscription_type, website, has_2fa, active, is_seeded
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1)
    `);
    const billId = {};
    for (const b of BILLS) {
      const verifiedAt = b.autopayVerified
        ? ymd(clampedDate(now.getFullYear(), now.getMonth(), Math.min(b.dueDay, 28)))
        : null;
      const r = insertBill.run(
        uid,
        b.name,
        catId[b.category] ?? null,
        b.dueDay,
        b.cycle,
        b.cycleType,
        toCents(b.amount),
        b.autopay ? 1 : 0,
        b.autopayVerified ? 'confirmed' : 'none',
        verifiedAt,
        b.interestRate ?? 0,
        b.currentBalance != null ? toCents(b.currentBalance) : null,
        b.minPayment != null ? toCents(b.minPayment) : null,
        b.snowballOrder ?? null,
        b.snowballInclude ?? 0,
        b.snowballExempt ?? 0,
        b.isSubscription ? 1 : 0,
        b.subscriptionType ?? null,
        b.website ?? null,
        b.has2fa ? 1 : 0,
      );
      billId[b.name] = r.lastInsertRowid;
      counts.bills++;
    }

    // ── Demo bank source + account (for matched/unmatched transactions) ──
    const source = db
      .prepare(
        "INSERT INTO data_sources (user_id, type, provider, name, status, is_seeded) VALUES (?, 'manual', 'demo', 'Demo Bank', 'active', 1)",
      )
      .run(uid);
    const sourceId = source.lastInsertRowid;
    const account = db
      .prepare(
        `
      INSERT INTO financial_accounts
        (user_id, data_source_id, provider_account_id, name, org_name, account_type, currency, balance, available_balance, monitored, is_seeded)
      VALUES (?, ?, 'demo-checking', 'Everyday Checking', 'Demo Bank', 'checking', 'USD', ?, ?, 1, 1)
    `,
      )
      .run(uid, sourceId, toCents(4210.55), toCents(4210.55));
    const accountId = account.lastInsertRowid;

    let txnSeq = 0;
    const insertTxn = db.prepare(`
      INSERT INTO transactions
        (user_id, data_source_id, account_id, provider_transaction_id, source_type, transaction_type,
         posted_date, transacted_at, amount, currency, description, payee, category,
         matched_bill_id, match_status, spending_category_id, is_seeded)
      VALUES (?, ?, ?, ?, 'manual', ?, ?, ?, ?, 'USD', ?, ?, ?, ?, ?, ?, 1)
    `);
    const addTxn = ({
      type = 'debit',
      date,
      amountDollars,
      description,
      payee,
      category = null,
      matchedBillId = null,
      matchStatus = 'unmatched',
      spendingCategoryId = null,
    }) => {
      const cents =
        type === 'credit' ? toCents(Math.abs(amountDollars)) : -toCents(Math.abs(amountDollars));
      const r = insertTxn.run(
        uid,
        sourceId,
        accountId,
        `demo-${++txnSeq}`,
        type,
        date,
        date,
        cents,
        description,
        payee,
        category,
        matchedBillId,
        matchStatus,
        spendingCategoryId,
      );
      counts.transactions++;
      return r.lastInsertRowid;
    };

    // ── Payment history + matched bank transactions + monthly overrides ──
    const insertPayment = db.prepare(`
      INSERT INTO payments (bill_id, amount, paid_date, method, notes, balance_delta, payment_source, transaction_id)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `);
    for (const b of BILLS) {
      const bid = billId[b.name];
      for (const offset of cycleOffsets(b)) {
        const y = now.getFullYear(),
          m = now.getMonth() + offset;
        const payDate = clampedDate(y, m, b.dueDay);
        if (payDate > now) continue; // future occurrence → naturally "upcoming", no payment

        // Deliberate skipped month → recorded as a monthly_bill_state skip, no payment.
        if (b.name === SKIP.bill && offset === -SKIP.monthsAgo) {
          db.prepare(
            'INSERT INTO monthly_bill_state (bill_id, year, month, is_skipped) VALUES (?, ?, ?, 1)',
          ).run(bid, payDate.getFullYear(), payDate.getMonth() + 1);
          counts.monthlyState++;
          continue;
        }
        // Current month: leave the designated overdue bills unpaid (past due, no payment).
        if (offset === 0 && OVERDUE_THIS_MONTH.has(b.name)) continue;

        let amount = driftedAmount(b, offset);
        if (b.name === OVERRIDE.bill && offset === -OVERRIDE.monthsAgo) amount = OVERRIDE.actual;

        // Debt bills: split into interest + principal so the balance pays down.
        let balanceDelta = null;
        if (b.currentBalance) {
          const interest = Math.round(
            (toCents(b.currentBalance) * (b.interestRate ?? 0)) / 100 / 12,
          );
          balanceDelta = -Math.max(0, toCents(amount) - interest);
        }

        // A subset of recent payments are auto-matched to a seeded bank transaction.
        let txnId = null,
          source_ = 'manual';
        if (BANK_MATCHED.has(b.name) && offset >= -1) {
          txnId = addTxn({
            date: ymd(payDate),
            amountDollars: amount,
            description: `${b.name.toUpperCase()} AUTOPAY`,
            payee: b.name,
            matchedBillId: bid,
            matchStatus: 'matched',
          });
          source_ = 'transaction_match';
        }

        insertPayment.run(
          bid,
          toCents(amount),
          ymd(payDate),
          'demo',
          null,
          balanceDelta,
          source_,
          txnId,
        );
        counts.payments++;
      }
    }

    // Amount-override state (actual recorded above; flag the month's actual_amount) + a note.
    {
      const od = clampedDate(now.getFullYear(), now.getMonth() - OVERRIDE.monthsAgo, 1);
      db.prepare(
        'INSERT INTO monthly_bill_state (bill_id, year, month, actual_amount) VALUES (?, ?, ?, ?)',
      ).run(billId[OVERRIDE.bill], od.getFullYear(), od.getMonth() + 1, toCents(OVERRIDE.actual));
      counts.monthlyState++;
      db.prepare(
        'INSERT INTO monthly_bill_state (bill_id, year, month, notes) VALUES (?, ?, ?, ?)',
      ).run(billId[NOTE.bill], now.getFullYear(), now.getMonth() + 1, NOTE.text);
      counts.monthlyState++;
      // Snooze one overdue bill's alert (Overdue Command Center demo).
      const snoozeUntil = ymd(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 5));
      db.prepare(
        'INSERT INTO monthly_bill_state (bill_id, year, month, snoozed_until) VALUES (?, ?, ?, ?)',
      ).run(billId['Trash Service'], now.getFullYear(), now.getMonth() + 1, snoozeUntil);
      counts.monthlyState++;
    }

    // ── Unmatched bank transactions that look like bills → Matches suggestions ──
    for (const name of ['Gas Utility', 'City Water Dept']) {
      const b = BILLS.find((x) => x.name === name);
      const d = clampedDate(now.getFullYear(), now.getMonth(), b.dueDay);
      if (d <= now)
        addTxn({
          date: ymd(d),
          amountDollars: b.amount,
          description: `${name.toUpperCase()} PMT`,
          payee: name,
        });
    }
    // ── Un-tracked subscription charges → Subscriptions recommendations ──
    for (const s of UNTRACKED_SUBS) {
      const d = clampedDate(now.getFullYear(), now.getMonth(), s.day);
      if (d <= now)
        addTxn({
          date: ymd(d),
          amountDollars: s.amount,
          description: `${s.payee} SUBSCRIPTION`,
          payee: s.payee,
        });
    }
    // ── Everyday spending transactions (categorised) → Spending page ──
    for (const merchant of SPENDING_MERCHANTS) {
      const scid = catId[merchant.category] ?? null;
      merchant.amounts.forEach((amt, i) => {
        const d = clampedDate(now.getFullYear(), now.getMonth() - (i % 2), 2 + ((i * 5) % 26));
        if (d <= now)
          addTxn({
            date: ymd(d),
            amountDollars: amt,
            description: merchant.payee,
            payee: merchant.payee,
            spendingCategoryId: scid,
          });
      });
    }

    // ── Planning: monthly income + starting amounts (recent months) ──
    for (let off = -3; off <= 0; off++) {
      const d = new Date(now.getFullYear(), now.getMonth() + off, 1);
      const y = d.getFullYear(),
        mo = d.getMonth() + 1;
      const hasIncome = db
        .prepare('SELECT 1 FROM monthly_income WHERE user_id = ? AND year = ? AND month = ?')
        .get(uid, y, mo);
      if (!hasIncome) {
        db.prepare(
          'INSERT INTO monthly_income (user_id, year, month, label, amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)',
        ).run(uid, y, mo, 'Paycheck', toCents(5200));
        counts.planning++;
      }
      const hasStart = db
        .prepare(
          'SELECT 1 FROM monthly_starting_amounts WHERE user_id = ? AND year = ? AND month = ?',
        )
        .get(uid, y, mo);
      if (!hasStart) {
        db.prepare(
          'INSERT INTO monthly_starting_amounts (user_id, year, month, first_amount, fifteenth_amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)',
        ).run(uid, y, mo, toCents(3200), toCents(2800));
        counts.planning++;
      }
    }

    // ── Spending budgets + auto-categorise rules ──
    for (const [cat, amount] of Object.entries(SPENDING_BUDGETS)) {
      if (!catId[cat]) continue;
      db.prepare(
        'INSERT INTO spending_budgets (user_id, category_id, year, month, amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)',
      ).run(uid, catId[cat], now.getFullYear(), now.getMonth() + 1, toCents(amount));
      counts.budgets++;
    }
    for (const rule of SPENDING_RULES) {
      if (!catId[rule.category]) continue;
      db.prepare(
        'INSERT INTO spending_category_rules (user_id, category_id, merchant, is_seeded) VALUES (?, ?, ?, 1)',
      ).run(uid, catId[rule.category], rule.merchant);
      counts.spendingRules++;
    }

    // ── Snowball: an active plan + the user's extra-payment setting ──
    const debtBills = BILLS.filter((b) => b.snowballInclude).sort(
      (a, b) => (a.snowballOrder ?? 99) - (b.snowballOrder ?? 99),
    );
    const snapshot = JSON.stringify({
      method: 'snowball',
      extra_payment_cents: toCents(200),
      order: debtBills.map((b) => ({
        bill_id: billId[b.name],
        name: b.name,
        balance_cents: toCents(b.currentBalance),
      })),
    });
    db.prepare(
      `INSERT INTO snowball_plans (user_id, name, method, status, extra_payment, plan_snapshot, is_seeded)
                VALUES (?, 'My Debt Snowball', 'snowball', 'active', ?, ?, 1)`,
    ).run(uid, toCents(200), snapshot);
    counts.snowballPlans++;
    // Only set the extra-payment if the user hasn't chosen one (don't clobber real data).
    db.prepare(
      'UPDATE users SET snowball_extra_payment = ? WHERE id = ? AND COALESCE(snowball_extra_payment, 0) = 0',
    ).run(toCents(200), uid);

    // ── Merchant → bill auto-match rules (cascade with their bill) ──
    for (const name of ['Netflix', 'Spotify']) {
      db.prepare(
        'INSERT INTO bill_merchant_rules (user_id, bill_id, merchant) VALUES (?, ?, ?)',
      ).run(uid, billId[name], name.toUpperCase());
      counts.merchantRules++;
    }

    // ── Calendar feed subscription token ──
    const crypto = require('crypto');
    db.prepare(
      'INSERT INTO calendar_tokens (user_id, token, label, active, is_seeded) VALUES (?, ?, ?, 1, 1)',
    ).run(uid, crypto.randomBytes(24).toString('hex'), 'Demo Calendar Feed');
    counts.calendarTokens++;
  })();

  return {
    billsCreated: counts.bills,
    categoriesCreated: counts.categories,
    paymentsCreated: counts.payments,
    transactionsCreated: counts.transactions,
    counts,
  };
}

// CLI entry
if (require.main === module) {
  try {
    const result = seedDemoData();
    console.log('\n✅ Seed complete:', JSON.stringify(result.counts, null, 2));
    process.exit(0);
  } catch (err) {
    console.error('Seed failed:', err.message);
    process.exit(err.code === 'ALREADY_SEEDED' ? 0 : 1);
  }
}

module.exports = { seedDemoData };
