'use strict';

const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const Database = require('better-sqlite3');
const { getDb } = require('../db/database');
const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService');
const { toCents } = require('../utils/money.mts');

const MAX_SQLITE_BYTES = 50 * 1024 * 1024;
const SESSION_TTL_HOURS = 24;
const REQUIRED_TABLES = ['export_metadata', 'categories', 'bills', 'payments', 'monthly_bill_state'];
const VALID_BILLING_CYCLES = new Set(['monthly', 'quarterly', 'annually', 'irregular']);
const VALID_AUTODRAFT = new Set(['none', 'pending', 'assumed_paid', 'confirmed']);
const VALID_PAYMENT_SOURCES = new Set(['manual', 'file_import', 'provider_sync']);

function importError(status, message, code, details = []) {
  const err = new Error(message);
  err.status = status;
  err.code = code;
  err.details = details;
  return err;
}

function sanitizeFilename(value) {
  return value
    ? String(value).replace(/[^a-zA-Z0-9._\-\s]/g, '').trim().slice(0, 255) || null
    : null;
}

function assertSqliteBuffer(buffer) {
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
    throw importError(400, 'SQLite export file is required.', 'USER_DB_IMPORT_FILE_REQUIRED');
  }
  if (buffer.length > MAX_SQLITE_BYTES) {
    throw importError(400, 'SQLite export file is too large. Maximum size is 50 MB.', 'USER_DB_IMPORT_FILE_TOO_LARGE');
  }
  if (buffer.length < 16 || buffer.subarray(0, 16).toString('binary') !== 'SQLite format 3\u0000') {
    throw importError(400, 'File is not a valid SQLite database export.', 'USER_DB_IMPORT_INVALID_SQLITE');
  }
}

function withTempSqlite(buffer, fn) {
  const file = path.join(os.tmpdir(), `bill-tracker-user-import-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.sqlite`);
  fs.writeFileSync(file, buffer, { mode: 0o600 });
  let src = null;
  try {
    src = new Database(file, { readonly: true, fileMustExist: true });
    src.prepare('SELECT name FROM sqlite_master LIMIT 1').get();
    return fn(src);
  } finally {
    try { src?.close(); } catch {}
    try { fs.unlinkSync(file); } catch {}
  }
}

function tableNames(db) {
  return new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name));
}

const IMPORT_TABLES = new Set([
  'categories', 'bills', 'payments',
  'monthly_bill_state', 'monthly_starting_amounts', 'notes',
]);

function tableColumns(db, table) {
  if (!IMPORT_TABLES.has(table)) throw new Error(`Import: unknown table '${table}'`);
  return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name));
}

function selectKnown(db, table, columns) {
  const cols = tableColumns(db, table);
  const selected = columns.filter(c => cols.has(c));
  if (!selected.length) return [];
  return db.prepare(`SELECT ${selected.join(', ')} FROM ${table}`).all();
}

function toInt(value, fallback = null) {
  if (value === undefined || value === null || value === '') return fallback;
  const n = Number(value);
  return Number.isInteger(n) ? n : fallback;
}

function toNumber(value, fallback = null) {
  if (value === undefined || value === null || value === '') return fallback;
  const n = Number(value);
  return Number.isFinite(n) ? n : fallback;
}

function cleanText(value, max = 1000) {
  if (value === undefined || value === null) return null;
  const s = String(value).trim();
  return s ? s.slice(0, max) : null;
}

function cleanDate(value) {
  const s = cleanText(value, 32);
  return /^\d{4}-\d{2}-\d{2}$/.test(s || '') ? s : null;
}

function normalizeName(value) {
  return String(value || '').trim().toLowerCase();
}

function parseMetadata(src) {
  const row = src.prepare("SELECT value FROM export_metadata WHERE key = 'metadata_json'").get();
  if (!row?.value) {
    throw importError(400, 'SQLite file is not a BillTracker user data export.', 'USER_DB_IMPORT_NOT_USER_EXPORT');
  }
  let metadata;
  try {
    metadata = JSON.parse(row.value);
  } catch {
    throw importError(400, 'SQLite export metadata could not be read.', 'USER_DB_IMPORT_BAD_METADATA');
  }
  if (metadata?.export_type !== 'user_data') {
    throw importError(400, 'SQLite file is not a user data export created by this app.', 'USER_DB_IMPORT_NOT_USER_EXPORT');
  }
  return metadata;
}

function sanitizeCategory(row) {
  const name = cleanText(row.name, 120);
  if (!name) return null;
  return {
    old_id: toInt(row.id),
    name,
    created_at: cleanText(row.created_at, 32),
    updated_at: cleanText(row.updated_at, 32),
  };
}

/**
 * Convert a money value from a source export to integer cents.
 * Pre-v1.03 exports store dollars (REAL); post-v1.03 exports already store
 * integer cents. Applying toCents() to a cents value would multiply it ×100,
 * so the caller detects the source's unit via its schema_migrations table.
 */
function importMoney(value, sourceIsCents) {
  if (value === null || value === undefined) return null;
  return sourceIsCents ? Math.round(Number(value)) : toCents(value);
}

/** True when the source DB already stores money in integer cents (v1.03+). */
function sourceUsesCents(src) {
  try {
    if (!tableNames(src).has('schema_migrations')) return false;
    return !!src.prepare("SELECT 1 FROM schema_migrations WHERE version = 'v1.03'").get();
  } catch {
    return false;
  }
}

function sanitizeBill(row, sourceIsCents) {
  const name = cleanText(row.name, 160);
  const dueDay = toInt(row.due_day);
  if (!name || dueDay < 1 || dueDay > 31) return null;
  const interestRate = toNumber(row.interest_rate, null);
  const cycleType = row.cycle_type
    ? String(row.cycle_type).trim().toLowerCase()
    : cycleTypeFromBillingCycle(row.billing_cycle);
  const normalizedCycleType = ['monthly', 'weekly', 'biweekly', 'quarterly', 'annual'].includes(cycleType)
    ? cycleType
    : 'monthly';
  return {
    old_id: toInt(row.id),
    name,
    category_id: toInt(row.category_id),
    due_day: dueDay,
    override_due_date: cleanText(row.override_due_date, 32),
    bucket: dueDay <= 14 ? '1st' : '15th',
    expected_amount: importMoney(Math.max(0, toNumber(row.expected_amount, 0) ?? 0), sourceIsCents),
    interest_rate: interestRate == null || interestRate < 0 || interestRate > 100 ? null : interestRate,
    billing_cycle: VALID_BILLING_CYCLES.has(row.billing_cycle) ? row.billing_cycle : billingCycleForCycleType(normalizedCycleType),
    cycle_type: normalizedCycleType,
    cycle_day: cleanText(row.cycle_day, 32),
    autopay_enabled: toInt(row.autopay_enabled, 0) ? 1 : 0,
    autodraft_status: VALID_AUTODRAFT.has(row.autodraft_status) ? row.autodraft_status : 'none',
    website: cleanText(row.website, 500),
    username: cleanText(row.username, 255),
    account_info: cleanText(row.account_info, 500),
    has_2fa: toInt(row.has_2fa, 0) ? 1 : 0,
    active: row.active === undefined ? 1 : (toInt(row.active, 1) ? 1 : 0),
    notes: cleanText(row.notes, 2000),
    created_at: cleanText(row.created_at, 32),
    updated_at: cleanText(row.updated_at, 32),
  };
}

function sanitizePayment(row, validBillIds, sourceIsCents) {
  const billId = toInt(row.bill_id);
  const amount = toNumber(row.amount);
  const paidDate = cleanDate(row.paid_date);
  const paymentSource = cleanText(row.payment_source, 64) || 'manual';
  if (!billId || !validBillIds.has(billId) || amount == null || amount < 0 || !paidDate) return null;
  return {
    old_id: toInt(row.id),
    bill_id: billId,
    amount: importMoney(amount, sourceIsCents),
    paid_date: paidDate,
    method: cleanText(row.method, 120),
    notes: cleanText(row.notes, 2000),
    payment_source: VALID_PAYMENT_SOURCES.has(paymentSource) ? paymentSource : 'manual',
    transaction_id: null,
    created_at: cleanText(row.created_at, 32),
    updated_at: cleanText(row.updated_at, 32),
  };
}

function sanitizeMonthlyState(row, validBillIds, sourceIsCents) {
  const billId = toInt(row.bill_id);
  const year = toInt(row.year);
  const month = toInt(row.month);
  if (!billId || !validBillIds.has(billId) || year < 2000 || year > 2100 || month < 1 || month > 12) return null;
  const actual = toNumber(row.actual_amount, null);
  return {
    old_id: toInt(row.id),
    bill_id: billId,
    year,
    month,
    actual_amount: actual == null || actual < 0 ? null : importMoney(actual, sourceIsCents),
    notes: cleanText(row.notes, 2000),
    is_skipped: toInt(row.is_skipped, 0) ? 1 : 0,
    created_at: cleanText(row.created_at, 32),
    updated_at: cleanText(row.updated_at, 32),
  };
}

function sanitizeMonthlyStartingAmounts(row, sourceIsCents) {
  const year = toInt(row.year);
  const month = toInt(row.month);
  if (year < 2000 || year > 2100 || month < 1 || month > 12) return null;
  return {
    old_id: toInt(row.id),
    year,
    month,
    first_amount: importMoney(Math.max(0, toNumber(row.first_amount, 0) ?? 0), sourceIsCents),
    fifteenth_amount: importMoney(Math.max(0, toNumber(row.fifteenth_amount, 0) ?? 0), sourceIsCents),
    other_amount: importMoney(Math.max(0, toNumber(row.other_amount, 0) ?? 0), sourceIsCents),
    notes: cleanText(row.notes, 2000),
    created_at: cleanText(row.created_at, 32),
    updated_at: cleanText(row.updated_at, 32),
  };
}

function readExportData(src) {
  const names = tableNames(src);
  const missing = REQUIRED_TABLES.filter(t => !names.has(t));
  if (missing.length) {
    throw importError(400, 'SQLite file is not a supported BillTracker user export.', 'USER_DB_IMPORT_UNSUPPORTED_SCHEMA', missing.map(table => ({
      table,
      message: 'Required user export table is missing',
    })));
  }

  const metadata = parseMetadata(src);
  // Pre-v1.03 exports store money in dollars; v1.03+ exports store integer cents.
  const sourceIsCents = sourceUsesCents(src);
  const categories = selectKnown(src, 'categories', ['id', 'name', 'created_at', 'updated_at'])
    .map(sanitizeCategory).filter(Boolean);
  const bills = selectKnown(src, 'bills', [
    'id', 'name', 'category_id', 'due_day', 'override_due_date', 'bucket', 'expected_amount', 'interest_rate',
    'billing_cycle', 'autopay_enabled', 'autodraft_status', 'website', 'username', 'account_info', 'has_2fa',
    'active', 'notes', 'created_at', 'updated_at',
  ]).map(row => sanitizeBill(row, sourceIsCents)).filter(Boolean);
  const validBillIds = new Set(bills.map(b => b.old_id).filter(Boolean));
  const payments = selectKnown(src, 'payments', [
    'id', 'bill_id', 'amount', 'paid_date', 'method', 'notes', 'payment_source', 'transaction_id', 'created_at', 'updated_at',
  ])
    .map(row => sanitizePayment(row, validBillIds, sourceIsCents)).filter(Boolean);
  const monthlyState = selectKnown(src, 'monthly_bill_state', ['id', 'bill_id', 'year', 'month', 'actual_amount', 'notes', 'is_skipped', 'created_at', 'updated_at'])
    .map(row => sanitizeMonthlyState(row, validBillIds, sourceIsCents)).filter(Boolean);
  const monthlyStartingAmounts = names.has('monthly_starting_amounts')
    ? selectKnown(src, 'monthly_starting_amounts', ['id', 'year', 'month', 'first_amount', 'fifteenth_amount', 'other_amount', 'notes', 'created_at', 'updated_at'])
      .map(row => sanitizeMonthlyStartingAmounts(row, sourceIsCents)).filter(Boolean)
    : [];
  const notes = names.has('notes')
    ? selectKnown(src, 'notes', ['type', 'bill_id', 'payment_id', 'monthly_state_id', 'year', 'month', 'notes'])
      .map(n => ({ ...n, notes: cleanText(n.notes, 2000) })).filter(n => n.notes)
    : [];

  return { metadata, categories, bills, payments, monthly_bill_state: monthlyState, monthly_starting_amounts: monthlyStartingAmounts, notes };
}

function existingLookups(db, userId) {
  const categories = db.prepare('SELECT id, name FROM categories WHERE user_id = ?').all(userId);
  const bills = db.prepare('SELECT id, name, due_day FROM bills WHERE user_id = ?').all(userId);
  return {
    categoryByName: new Map(categories.map(c => [normalizeName(c.name), c])),
    billByKey: new Map(bills.map(b => [`${normalizeName(b.name)}|${b.due_day}`, b])),
  };
}

function buildPreview(userId, data, originalFilename) {
  const db = getDb();
  const lookups = existingLookups(db, userId);
  const categoryPlan = data.categories.map(c => {
    const existing = lookups.categoryByName.get(normalizeName(c.name));
    return {
      old_id: c.old_id,
      name: c.name,
      action: existing ? 'skip' : 'create',
      existing_id: existing?.id ?? null,
      reason: existing ? 'Category already exists for this user' : 'Category is missing for this user',
    };
  });

  const categoryMap = new Map(categoryPlan.map(c => [c.old_id, c]));
  const billPlan = data.bills.map(b => {
    const existing = lookups.billByKey.get(`${normalizeName(b.name)}|${b.due_day}`);
    const category = categoryMap.get(b.category_id);
    return {
      old_id: b.old_id,
      name: b.name,
      due_day: b.due_day,
      category_name: category?.name ?? null,
      action: existing ? 'skip' : 'create',
      existing_id: existing?.id ?? null,
      reason: existing ? 'Bill with same name and due day already exists for this user' : 'Bill is missing for this user',
    };
  });

  const billPlanByOldId = new Map(billPlan.map(b => [b.old_id, b]));
  const paymentPlan = data.payments.map(p => ({
    old_id: p.old_id,
    bill_old_id: p.bill_id,
    action: billPlanByOldId.has(p.bill_id) ? 'create_or_skip_duplicate' : 'conflict',
    reason: billPlanByOldId.has(p.bill_id) ? 'Will import if no duplicate payment exists' : 'Referenced bill is not present in export',
  }));
  const monthlyPlan = data.monthly_bill_state.map(m => ({
    old_id: m.old_id,
    bill_old_id: m.bill_id,
    year: m.year,
    month: m.month,
    action: billPlanByOldId.has(m.bill_id) ? 'create_or_skip_duplicate' : 'conflict',
    reason: billPlanByOldId.has(m.bill_id) ? 'Will import if monthly state is missing' : 'Referenced bill is not present in export',
  }));
  const monthlyStartingAmountsPlan = data.monthly_starting_amounts.map(m => ({
    old_id: m.old_id,
    year: m.year,
    month: m.month,
    action: 'create_or_skip_duplicate',
    reason: 'Will import if monthly starting amounts are missing',
  }));

  const summary = {
    categories: {
      total: data.categories.length,
      create: categoryPlan.filter(x => x.action === 'create').length,
      skip: categoryPlan.filter(x => x.action === 'skip').length,
      conflict: 0,
    },
    bills: {
      total: data.bills.length,
      create: billPlan.filter(x => x.action === 'create').length,
      skip: billPlan.filter(x => x.action === 'skip').length,
      conflict: 0,
    },
    payments: {
      total: data.payments.length,
      create: data.payments.length,
      skip: 0,
      conflict: paymentPlan.filter(x => x.action === 'conflict').length,
    },
    monthly_bill_state: {
      total: data.monthly_bill_state.length,
      create: data.monthly_bill_state.length,
      skip: 0,
      conflict: monthlyPlan.filter(x => x.action === 'conflict').length,
    },
    monthly_starting_amounts: {
      total: data.monthly_starting_amounts.length,
      create: data.monthly_starting_amounts.length,
      skip: 0,
      conflict: 0,
    },
    notes: {
      total: data.notes.length,
      create: 0,
      skip: data.notes.length,
      conflict: 0,
    },
  };

  const warnings = [
    'Conflicts and duplicates are skipped by default.',
    'This import does not restore users, passwords, sessions, admin settings, SMTP credentials, backups, or server paths.',
  ];

  return {
    import_type: 'user_db',
    source_filename: originalFilename,
    metadata: data.metadata,
    counts: {
      bills: data.bills.length,
      categories: data.categories.length,
      payments: data.payments.length,
      monthly_bill_state: data.monthly_bill_state.length,
      monthly_starting_amounts: data.monthly_starting_amounts.length,
      notes: data.notes.length,
    },
    summary,
    proposed_actions: {
      categories: categoryPlan.slice(0, 100),
      bills: billPlan.slice(0, 100),
      payments: paymentPlan.slice(0, 100),
      monthly_bill_state: monthlyPlan.slice(0, 100),
      monthly_starting_amounts: monthlyStartingAmountsPlan.slice(0, 100),
    },
    warnings,
  };
}

function saveSession(db, userId, sessionData) {
  const id = crypto.randomUUID();
  const now = new Date();
  const expires = new Date(now.getTime() + SESSION_TTL_HOURS * 60 * 60 * 1000);
  db.prepare(`
    INSERT INTO import_sessions (id, user_id, created_at, expires_at, preview_json)
    VALUES (?, ?, ?, ?, ?)
  `).run(id, userId, now.toISOString(), expires.toISOString(), JSON.stringify(sessionData));
  return id;
}

function loadSession(db, userId, importSessionId) {
  const row = db.prepare(`
    SELECT preview_json FROM import_sessions
    WHERE id = ? AND user_id = ? AND expires_at > ?
  `).get(importSessionId, userId, new Date().toISOString());
  if (!row) {
    throw importError(400, 'Import session is invalid or expired. Preview the SQLite export again.', 'USER_DB_IMPORT_SESSION_INVALID');
  }
  const data = JSON.parse(row.preview_json);
  if (data.import_type !== 'user_db') {
    throw importError(400, 'Import session is not a SQLite user export preview.', 'USER_DB_IMPORT_SESSION_TYPE');
  }
  return data;
}

async function previewUserDbImport(userId, buffer, options = {}) {
  assertSqliteBuffer(buffer);
  const originalFilename = sanitizeFilename(options.original_filename);
  return withTempSqlite(buffer, (src) => {
    const data = readExportData(src);
    const preview = buildPreview(userId, data, originalFilename);
    const db = getDb();
    const importSessionId = saveSession(db, userId, {
      import_type: 'user_db',
      source_filename: originalFilename,
      metadata: data.metadata,
      data,
      preview,
    });
    return {
      import_session_id: importSessionId,
      ...preview,
    };
  });
}

function ensureCategory(db, userId, category, summary, details) {
  const existing = db.prepare('SELECT id FROM categories WHERE user_id = ? AND LOWER(name) = LOWER(?)').get(userId, category.name);
  if (existing) {
    summary.rows_skipped++;
    summary.rows_conflicted++;
    details.categories.skipped++;
    return existing.id;
  }
  const result = db.prepare('INSERT INTO categories (user_id, name) VALUES (?, ?)').run(userId, category.name);
  summary.rows_created++;
  details.categories.created++;
  return result.lastInsertRowid;
}

function ensureBill(db, userId, bill, categoryId, summary, details) {
  const existing = db.prepare('SELECT id FROM bills WHERE user_id = ? AND LOWER(name) = LOWER(?) AND due_day = ?')
    .get(userId, bill.name, bill.due_day);
  if (existing) {
    summary.rows_skipped++;
    summary.rows_conflicted++;
    details.bills.skipped++;
    return existing.id;
  }
  const result = db.prepare(`
    INSERT INTO bills
      (user_id, name, category_id, due_day, override_due_date, bucket, expected_amount, interest_rate,
       billing_cycle, cycle_type, cycle_day, autopay_enabled, autodraft_status, website, username, account_info, has_2fa,
       active, notes)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    userId,
    bill.name,
    categoryId || null,
    bill.due_day,
    bill.override_due_date || null,
    bill.bucket,
    bill.expected_amount,
    bill.interest_rate,
    billingCycleForCycleType(bill.cycle_type),
    bill.cycle_type,
    bill.cycle_day || null,
    bill.autopay_enabled,
    bill.autodraft_status,
    bill.website,
    bill.username,
    bill.account_info,
    bill.has_2fa,
    bill.active,
    bill.notes,
  );
  summary.rows_created++;
  details.bills.created++;
  return result.lastInsertRowid;
}

function importPayment(db, targetBillId, payment, summary, details) {
  const duplicate = db.prepare(`
    SELECT p.id FROM payments p
    WHERE p.bill_id = ? AND p.paid_date = ? AND p.amount = ? AND COALESCE(p.method, '') = COALESCE(?, '')
      AND p.deleted_at IS NULL
  `).get(targetBillId, payment.paid_date, payment.amount, payment.method);
  if (duplicate) {
    summary.rows_skipped++;
    summary.rows_conflicted++;
    details.payments.skipped++;
    return;
  }
  db.prepare(`
    INSERT INTO payments (bill_id, amount, paid_date, method, notes, payment_source, transaction_id)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).run(
    targetBillId,
    payment.amount,
    payment.paid_date,
    payment.method,
    payment.notes,
    payment.payment_source || 'manual',
    payment.transaction_id,
  );
  summary.rows_created++;
  details.payments.created++;
}

function importMonthlyState(db, targetBillId, row, summary, details) {
  const duplicate = db.prepare(`
    SELECT id FROM monthly_bill_state WHERE bill_id = ? AND year = ? AND month = ?
  `).get(targetBillId, row.year, row.month);
  if (duplicate) {
    summary.rows_skipped++;
    summary.rows_conflicted++;
    details.monthly_bill_state.skipped++;
    return;
  }
  db.prepare(`
    INSERT INTO monthly_bill_state (bill_id, year, month, actual_amount, notes, is_skipped)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(targetBillId, row.year, row.month, row.actual_amount, row.notes, row.is_skipped);
  summary.rows_created++;
  details.monthly_bill_state.created++;
}

function importMonthlyStartingAmounts(db, userId, row, summary, details) {
  const duplicate = db.prepare(`
    SELECT id FROM monthly_starting_amounts WHERE user_id = ? AND year = ? AND month = ?
  `).get(userId, row.year, row.month);
  if (duplicate) {
    summary.rows_skipped++;
    summary.rows_conflicted++;
    details.monthly_starting_amounts.skipped++;
    return;
  }
  db.prepare(`
    INSERT INTO monthly_starting_amounts (user_id, year, month, first_amount, fifteenth_amount, other_amount, notes)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).run(userId, row.year, row.month, row.first_amount, row.fifteenth_amount, row.other_amount, row.notes);
  summary.rows_created++;
  details.monthly_starting_amounts.created++;
}

async function applyUserDbImport(userId, importSessionId, options = {}) {
  if (options.overwrite) {
    throw importError(400, 'Overwrite is not supported for user SQLite imports. Existing data is skipped.', 'USER_DB_IMPORT_OVERWRITE_UNSUPPORTED');
  }

  const db = getDb();
  const session = loadSession(db, userId, importSessionId);
  const { data } = session;
  const summary = {
    import_type: 'user_db',
    rows_created: 0,
    rows_updated: 0,
    rows_skipped: 0,
    rows_conflicted: 0,
    rows_errored: 0,
  };
  const details = {
    categories: { created: 0, skipped: 0, errored: 0 },
    bills: { created: 0, skipped: 0, errored: 0 },
    payments: { created: 0, skipped: 0, errored: 0 },
    monthly_bill_state: { created: 0, skipped: 0, errored: 0 },
    monthly_starting_amounts: { created: 0, skipped: 0, errored: 0 },
    notes: { created: 0, skipped: data.notes?.length || 0, errored: 0 },
  };
  summary.rows_skipped += details.notes.skipped;

  const tx = db.transaction(() => {
    const categoryIdMap = new Map();
    for (const category of data.categories) {
      categoryIdMap.set(category.old_id, ensureCategory(db, userId, category, summary, details));
    }

    const billIdMap = new Map();
    for (const bill of data.bills) {
      const targetCategoryId = bill.category_id ? categoryIdMap.get(bill.category_id) : null;
      billIdMap.set(bill.old_id, ensureBill(db, userId, bill, targetCategoryId, summary, details));
    }

    for (const payment of data.payments) {
      const targetBillId = billIdMap.get(payment.bill_id);
      if (!targetBillId) {
        summary.rows_skipped++;
        summary.rows_conflicted++;
        details.payments.skipped++;
        continue;
      }
      importPayment(db, targetBillId, payment, summary, details);
    }

    for (const row of data.monthly_bill_state) {
      const targetBillId = billIdMap.get(row.bill_id);
      if (!targetBillId) {
        summary.rows_skipped++;
        summary.rows_conflicted++;
        details.monthly_bill_state.skipped++;
        continue;
      }
      importMonthlyState(db, targetBillId, row, summary, details);
    }

    for (const row of data.monthly_starting_amounts) {
      importMonthlyStartingAmounts(db, userId, row, summary, details);
    }

    db.prepare(`
      INSERT INTO import_history
        (user_id, imported_at, source_filename, file_type, sheet_name, rows_parsed,
         rows_created, rows_updated, rows_skipped, rows_ambiguous, rows_errored,
         options_json, summary_json)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      userId,
      new Date().toISOString(),
      session.source_filename || null,
      'sqlite',
      'User SQLite export',
      data.categories.length + data.bills.length + data.payments.length + data.monthly_bill_state.length + data.monthly_starting_amounts.length + (data.notes?.length || 0),
      summary.rows_created,
      summary.rows_updated,
      summary.rows_skipped,
      summary.rows_conflicted,
      summary.rows_errored,
      JSON.stringify({ overwrite: false }),
      JSON.stringify({ ...summary, details }),
    );

    db.prepare('DELETE FROM import_sessions WHERE id = ? AND user_id = ?').run(importSessionId, userId);
  });

  tx();
  return { ...summary, details };
}

module.exports = {
  previewUserDbImport,
  applyUserDbImport,
};
