'use strict';

const { normalizeMerchant } = require('./subscriptionService');
const { localDateString } = require('../utils/dates.mts');
const { toCents, fromCents } = require('../utils/money.mts');

// Spending = unmatched outflows (amount < 0) that haven't been ignored.
// Bill-matched transactions are excluded so there's no double-counting.
const SPENDING_WHERE = `
  t.amount < 0
  AND t.ignored = 0
  AND t.match_status != 'matched'
  AND t.user_id = ?
`;

function monthRange(year, month) {
  const start = `${year}-${String(month).padStart(2, '0')}-01`;
  const end   = localDateString(new Date(year, month, 0)); // last day
  return { start, end };
}

function shiftMonth(year, month, delta) {
  let m = month + delta, y = year;
  while (m < 1)  { m += 12; y--; }
  while (m > 12) { m -= 12; y++; }
  return { year: y, month: m };
}

function cents(raw) {
  return Math.abs(Number(raw)) / 100;
}

// ── Summary ──────────────────────────────────────────────────────────────────

function getSpendingSummary(db, userId, year, month) {
  const { start, end } = monthRange(year, month);
  const dateRangeParams = [start, end, start + 'T00:00:00', end + 'T23:59:59'];
  const DATE_RANGE_SQL = '(t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))';

  // Every spending-enabled category, even with $0 activity this month (YNAB-style: budgeted
  // categories are always visible, not just ones with transactions).
  const categoryRows = db.prepare(`
    SELECT
      c.id        AS category_id,
      c.name      AS category_name,
      c.group_id  AS group_id,
      cg.name     AS group_name,
      COALESCE(SUM(ABS(t.amount)), 0) AS total_cents,
      COUNT(t.id)                     AS tx_count
    FROM categories c
    LEFT JOIN category_groups cg ON cg.id = c.group_id
    LEFT JOIN transactions t ON t.spending_category_id = c.id
      AND ${SPENDING_WHERE} AND ${DATE_RANGE_SQL}
    WHERE c.user_id = ? AND c.deleted_at IS NULL AND c.spending_enabled = 1
    GROUP BY c.id
    ORDER BY CASE WHEN c.sort_order IS NULL THEN 1 ELSE 0 END, c.sort_order ASC, c.name COLLATE NOCASE ASC
  `).all(userId, ...dateRangeParams, userId);

  // Uncategorized outflows
  const uncatRow = db.prepare(`
    SELECT COALESCE(SUM(ABS(t.amount)), 0) AS total_cents, COUNT(t.id) AS tx_count
    FROM transactions t
    WHERE ${SPENDING_WHERE} AND t.spending_category_id IS NULL AND ${DATE_RANGE_SQL}
  `).get(userId, ...dateRangeParams);

  // Outflows categorized to a category that isn't (or is no longer) spending-enabled —
  // bundled into a single "Other categories" row so totals stay accurate without polluting
  // the spending-enabled category list.
  const otherRow = db.prepare(`
    SELECT COALESCE(SUM(ABS(t.amount)), 0) AS total_cents, COUNT(t.id) AS tx_count
    FROM transactions t
    LEFT JOIN categories c ON c.id = t.spending_category_id
    WHERE ${SPENDING_WHERE} AND ${DATE_RANGE_SQL}
      AND t.spending_category_id IS NOT NULL
      AND (c.id IS NULL OR c.deleted_at IS NOT NULL OR c.spending_enabled = 0)
  `).get(userId, ...dateRangeParams);

  const budgets = db.prepare(`
    SELECT category_id, amount FROM spending_budgets
    WHERE user_id = ? AND year = ? AND month = ?
  `).all(userId, year, month);
  const budgetMap = new Map(budgets.map(b => [b.category_id, b.amount]));

  // 3-month spending average per category (the 3 calendar months before the requested one)
  const prev1 = monthRange(shiftMonth(year, month, -1).year, shiftMonth(year, month, -1).month);
  const prev3 = monthRange(shiftMonth(year, month, -3).year, shiftMonth(year, month, -3).month);
  const avgRows = db.prepare(`
    SELECT t.spending_category_id AS category_id, COALESCE(SUM(ABS(t.amount)), 0) AS total_cents
    FROM transactions t
    WHERE ${SPENDING_WHERE} AND t.spending_category_id IS NOT NULL
      AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
    GROUP BY t.spending_category_id
  `).all(userId, prev3.start, prev1.end, prev3.start + 'T00:00:00', prev1.end + 'T23:59:59');
  const avgMap = new Map(avgRows.map(r => [r.category_id, r.total_cents / 3]));

  let totalCents = 0;
  const byCategory = categoryRows.map(r => {
    totalCents += r.total_cents;
    return {
      category_id:   r.category_id,
      category_name: r.category_name,
      group_id:      r.group_id ?? null,
      group_name:    r.group_name ?? null,
      amount:        r.total_cents / 100,
      tx_count:      r.tx_count,
      budget:        fromCents(budgetMap.get(r.category_id) ?? null),
      avg_3mo:       fromCents(avgMap.get(r.category_id) ?? 0),
    };
  });

  if (uncatRow.tx_count > 0) {
    totalCents += uncatRow.total_cents;
    byCategory.push({
      category_id: null, category_name: '(Uncategorized)', group_id: null, group_name: null,
      amount: uncatRow.total_cents / 100, tx_count: uncatRow.tx_count, budget: null, avg_3mo: null,
    });
  }

  if (otherRow.tx_count > 0) {
    totalCents += otherRow.total_cents;
    byCategory.push({
      category_id: 'other', category_name: 'Other categories', group_id: null, group_name: null,
      amount: otherRow.total_cents / 100, tx_count: otherRow.tx_count, budget: null, avg_3mo: null,
    });
  }

  // Attach pct_of_total
  byCategory.forEach(c => {
    c.pct_of_total = totalCents > 0 ? Math.round(c.amount / (totalCents / 100) * 100) / 100 : 0;
  });

  const uncategorized_amount = uncatRow.total_cents / 100;
  const uncategorized_count  = uncatRow.tx_count;

  // Income (positive unmatched transactions this month)
  const incomeRow = db.prepare(`
    SELECT COALESCE(SUM(t.amount), 0) AS total FROM transactions t
    WHERE t.user_id = ? AND t.ignored = 0 AND t.amount > 0
      AND t.match_status != 'matched'
      AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
  `).get(userId, start, end, start + 'T00:00:00', end + 'T23:59:59');

  const categoryGroups = db.prepare(`
    SELECT id, name, sort_order FROM category_groups
    WHERE user_id = ? ORDER BY sort_order ASC, name COLLATE NOCASE ASC
  `).all(userId);

  return {
    year, month,
    total_spending:        totalCents / 100,
    uncategorized_amount,
    uncategorized_count,
    by_category:           byCategory,
    category_groups:       categoryGroups,
    income:                (incomeRow?.total ?? 0) / 100,
  };
}

// ── Transactions ─────────────────────────────────────────────────────────────

function getSpendingTransactions(db, userId, year, month, {
  categoryId = undefined,
  uncategorizedOnly = false,
  page = 1,
  limit = 50,
} = {}) {
  const { start, end } = monthRange(year, month);
  const offset = (Math.max(1, page) - 1) * limit;

  let filter = '';
  const params = [userId, start, end, start + 'T00:00:00', end + 'T23:59:59'];

  if (uncategorizedOnly) {
    filter = 'AND t.spending_category_id IS NULL';
  } else if (categoryId !== undefined) {
    if (categoryId === null) {
      filter = 'AND t.spending_category_id IS NULL';
    } else {
      filter = 'AND t.spending_category_id = ?';
      params.push(categoryId);
    }
  }

  const rows = db.prepare(`
    SELECT
      t.id, t.amount, t.payee, t.description, t.memo,
      t.posted_date, t.transacted_at, t.spending_category_id,
      c.name AS category_name
    FROM transactions t
    LEFT JOIN categories c ON c.id = t.spending_category_id AND c.deleted_at IS NULL
    WHERE ${SPENDING_WHERE} ${filter}
      AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
    ORDER BY COALESCE(t.posted_date, DATE(t.transacted_at)) DESC, t.id DESC
    LIMIT ? OFFSET ?
  `).all(...params, limit, offset);

  const total = db.prepare(`
    SELECT COUNT(*) AS n FROM transactions t
    WHERE ${SPENDING_WHERE} ${filter}
      AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
  `).get(...params).n;

  return {
    transactions: rows.map(r => ({
      id:                  r.id,
      amount:              cents(r.amount),
      payee:               r.payee || r.description || r.memo || '(Unknown)',
      date:                r.posted_date || (r.transacted_at ? String(r.transacted_at).slice(0, 10) : null),
      spending_category_id:   r.spending_category_id,
      spending_category_name: r.category_name ?? null,
    })),
    total,
    page,
    pages: Math.ceil(total / limit),
  };
}

// ── Categorize ───────────────────────────────────────────────────────────────

function categorizeTransaction(db, userId, txId, categoryId, saveMerchantRule = false) {
  const tx = db.prepare('SELECT * FROM transactions WHERE id = ? AND user_id = ?').get(txId, userId);
  if (!tx) throw Object.assign(new Error('Transaction not found'), { status: 404 });

  db.prepare("UPDATE transactions SET spending_category_id=?, updated_at=datetime('now') WHERE id=? AND user_id=?")
    .run(categoryId ?? null, txId, userId);

  if (saveMerchantRule && categoryId) {
    const merchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
    if (merchant && merchant.length >= 3) {
      try {
        db.prepare(`
          INSERT INTO spending_category_rules (user_id, category_id, merchant)
          VALUES (?, ?, ?)
          ON CONFLICT(user_id, merchant) DO UPDATE SET category_id=excluded.category_id
        `).run(userId, categoryId, merchant);
        // Auto-mark this category as spending-enabled
        db.prepare("UPDATE categories SET spending_enabled=1, updated_at=datetime('now') WHERE id=? AND user_id=?")
          .run(categoryId, userId);
        applySpendingCategoryRules(db, userId, merchant);
      } catch { /* safe to ignore */ }
    }
  }
}

// ── Auto-categorization ──────────────────────────────────────────────────────

function applySpendingCategoryRules(db, userId, onlyMerchant = null) {
  let rules;
  try {
    const q = onlyMerchant
      ? db.prepare('SELECT merchant, category_id FROM spending_category_rules WHERE user_id=? AND merchant=?')
          .all(userId, onlyMerchant)
      : db.prepare('SELECT merchant, category_id FROM spending_category_rules WHERE user_id=?')
          .all(userId);
    rules = q;
  } catch { return 0; }
  if (!rules.length) return 0;

  let applied = 0;
  const update = db.prepare("UPDATE transactions SET spending_category_id=?, updated_at=datetime('now') WHERE id=? AND user_id=?");

  const txRows = db.prepare(`
    SELECT id, payee, description, memo FROM transactions
    WHERE user_id=? AND amount<0 AND ignored=0 AND match_status!='matched' AND spending_category_id IS NULL
  `).all(userId);

  db.transaction(() => {
    for (const tx of txRows) {
      const merchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
      if (!merchant) continue;
      const rule = rules.find(r => merchantMatches(merchant, r.merchant));
      if (rule) { update.run(rule.category_id, tx.id, userId); applied++; }
    }
  })();

  return applied;
}

function merchantMatches(txMerchant, ruleMerchant) {
  if (!txMerchant || !ruleMerchant) return false;
  if (txMerchant === ruleMerchant) return true;
  const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const wb  = s => new RegExp(`(^|\\s)${esc(s)}(\\s|$)`);
  return wb(ruleMerchant).test(txMerchant) || wb(txMerchant).test(ruleMerchant);
}

// ── Budgets ──────────────────────────────────────────────────────────────────

function getSpendingBudgets(db, userId, year, month) {
  const rows = db.prepare(`
    SELECT sb.category_id, sb.amount, c.name AS category_name
    FROM spending_budgets sb
    JOIN categories c ON c.id = sb.category_id AND c.deleted_at IS NULL
    WHERE sb.user_id=? AND sb.year=? AND sb.month=?
  `).all(userId, year, month);
  return rows.map(r => ({ ...r, amount: fromCents(r.amount) }));
}

function setSpendingBudget(db, userId, categoryId, year, month, amount) {
  if (amount === null || amount === undefined) {
    db.prepare('DELETE FROM spending_budgets WHERE user_id=? AND category_id=? AND year=? AND month=?')
      .run(userId, categoryId, year, month);
  } else {
    db.prepare(`
      INSERT INTO spending_budgets (user_id, category_id, year, month, amount, updated_at)
      VALUES (?, ?, ?, ?, ?, datetime('now'))
      ON CONFLICT(user_id, category_id, year, month) DO UPDATE SET
        amount=excluded.amount, updated_at=datetime('now')
    `).run(userId, categoryId, year, month, toCents(amount));
  }
}

// ── Category rules ───────────────────────────────────────────────────────────

function getSpendingCategoryRules(db, userId) {
  return db.prepare(`
    SELECT r.id, r.merchant, r.category_id, c.name AS category_name
    FROM spending_category_rules r
    JOIN categories c ON c.id = r.category_id AND c.deleted_at IS NULL
    WHERE r.user_id=?
    ORDER BY r.merchant ASC
  `).all(userId);
}

function addSpendingCategoryRule(db, userId, categoryId, merchant) {
  const normalized = normalizeMerchant(merchant);
  if (!normalized || normalized.length < 2) throw Object.assign(new Error('Merchant name too short'), { status: 400 });
  db.prepare(`
    INSERT INTO spending_category_rules (user_id, category_id, merchant)
    VALUES (?, ?, ?)
    ON CONFLICT(user_id, merchant) DO UPDATE SET category_id=excluded.category_id
  `).run(userId, categoryId, normalized);
  // Auto-mark this category as spending-enabled so it shows in the picker
  try {
    db.prepare("UPDATE categories SET spending_enabled=1, updated_at=datetime('now') WHERE id=? AND user_id=?")
      .run(categoryId, userId);
  } catch { /* non-critical */ }
  applySpendingCategoryRules(db, userId, normalized);
}

function deleteSpendingCategoryRule(db, userId, ruleId) {
  db.prepare('DELETE FROM spending_category_rules WHERE id=? AND user_id=?').run(ruleId, userId);
}

// ── Income ───────────────────────────────────────────────────────────────────

function getIncomeTransactions(db, userId, year, month, { page = 1, limit = 50, includeIgnored = false } = {}) {
  const { start, end } = monthRange(year, month);
  const offset = (Math.max(1, page) - 1) * limit;
  const ignoredFilter = includeIgnored ? '' : 'AND ignored = 0';

  try {
    const rows = db.prepare(`
      SELECT id, amount, payee, description, memo, posted_date, transacted_at, ignored
      FROM transactions
      WHERE user_id = ? AND amount > 0 ${ignoredFilter} AND match_status != 'matched'
        AND (posted_date BETWEEN ? AND ? OR (posted_date IS NULL AND transacted_at BETWEEN ? AND ?))
      ORDER BY ignored ASC, COALESCE(posted_date, DATE(transacted_at)) DESC, id DESC
      LIMIT ? OFFSET ?
    `).all(userId, start, end, start + 'T00:00:00', end + 'T23:59:59', limit, offset);

    const total = db.prepare(`
      SELECT COUNT(*) AS n FROM transactions
      WHERE user_id = ? AND amount > 0 ${ignoredFilter} AND match_status != 'matched'
        AND (posted_date BETWEEN ? AND ? OR (posted_date IS NULL AND transacted_at BETWEEN ? AND ?))
    `).get(userId, start, end, start + 'T00:00:00', end + 'T23:59:59').n;

    return {
      transactions: rows.map(r => ({
        id:      r.id,
        amount:  Math.abs(Number(r.amount)) / 100,
        payee:   r.payee || r.description || r.memo || '(Unknown)',
        date:    r.posted_date || (r.transacted_at ? String(r.transacted_at).slice(0, 10) : null),
        ignored: r.ignored === 1,
      })),
      total,
      page,
      pages: Math.ceil(total / limit),
    };
  } catch (err) {
    console.error('[getIncomeTransactions]', err.message);
    return { transactions: [], total: 0, page: 1, pages: 1 };
  }
}

module.exports = {
  getSpendingSummary,
  getSpendingTransactions,
  categorizeTransaction,
  applySpendingCategoryRules,
  getSpendingBudgets,
  setSpendingBudget,
  getSpendingCategoryRules,
  addSpendingCategoryRule,
  deleteSpendingCategoryRule,
  getIncomeTransactions,
};
