// @ts-nocheck — converted to .cts (runs via Node type-stripping); rich typing deferred (large dynamic SQLite user-DB importer). TODO: type incrementally.
'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.cts');
const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService.cts');
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 Bill Tracker 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 Bill Tracker 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,
};
