const { all, audit, now, one, parseJson, run, tx } = require('../db');

// Stored, editable general-ledger journal entries per book/company. Independent of the computed
// depreciation rollup (bookLedger) — this is a manual/imported journal layer. Every change is logged
// through audit() with full before/after JSON for the Activity & Audit Trail.

function round2(value) {
  return Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100;
}

function num(value) {
  if (value === '' || value === null || value === undefined) return 0;
  const n = Number(String(value).replace(/[$,]/g, ''));
  return Number.isFinite(n) ? n : 0;
}

function strOrNull(value) {
  const s = value === null || value === undefined ? '' : String(value).trim();
  return s || null;
}

function yearOf(dateStr) {
  return dateStr ? Number(String(dateStr).slice(0, 4)) || null : null;
}

function entryFromRow(row) {
  return { ...row };
}

// ---- Read -------------------------------------------------------------------

function listEntries(filters = {}, companyId) {
  const entries = all(
    `SELECT g.*, a.asset_id AS asset_code
     FROM gl_entries g
     LEFT JOIN assets a ON a.id = g.asset_id
     WHERE g.entity_id = ? AND g.book_code = ?
       AND (? IS NULL OR g.fiscal_year = ?)
       AND (? IS NULL OR g.account = ?)
       AND (? IS NULL OR (g.account LIKE '%' || ? || '%' OR g.description LIKE '%' || ? || '%' OR g.reference LIKE '%' || ? || '%'))
     ORDER BY g.entry_date DESC, g.id DESC
     LIMIT 2000`,
    [
      companyId, filters.book,
      filters.year || null, filters.year || null,
      filters.account || null, filters.account || null,
      filters.search || null, filters.search || null, filters.search || null, filters.search || null
    ]
  ).map(entryFromRow);
  const debit = round2(entries.reduce((s, e) => s + Number(e.debit || 0), 0));
  const credit = round2(entries.reduce((s, e) => s + Number(e.credit || 0), 0));
  return { entries, summary: { count: entries.length, debit, credit, balance: round2(debit - credit) } };
}

function getEntry(id, companyId) {
  return one('SELECT * FROM gl_entries WHERE id = ? AND entity_id = ?', [id, companyId]) || null;
}

// ---- Normalize / validate ---------------------------------------------------

// Tolerant field picker: matches a parsed row's keys case/space/underscore-insensitively.
function pick(raw, ...aliases) {
  const map = {};
  for (const key of Object.keys(raw || {})) map[key.toLowerCase().replace(/[\s_]+/g, '')] = raw[key];
  for (const alias of aliases) {
    const value = map[alias.toLowerCase().replace(/[\s_]+/g, '')];
    if (value !== undefined && value !== null && value !== '') return value;
  }
  return undefined;
}

// Map a raw import row (tolerant headers) to the normalized GL entry shape.
function normalizeRow(raw) {
  const entry_date = String(pick(raw, 'entry_date', 'date', 'posting_date', 'posted') || '').slice(0, 10);
  return {
    entry_date,
    fiscal_year: yearOf(entry_date),
    account: String(pick(raw, 'account', 'gl_account', 'account_number', 'acct') || '').trim(),
    account_type: strOrNull(pick(raw, 'account_type', 'type', 'account_name')),
    description: strOrNull(pick(raw, 'description', 'memo', 'desc', 'narrative')),
    reference: strOrNull(pick(raw, 'reference', 'ref', 'document', 'doc')),
    debit: round2(num(pick(raw, 'debit', 'dr'))),
    credit: round2(num(pick(raw, 'credit', 'cr'))),
    external_id: strOrNull(pick(raw, 'external_id', 'id', 'gl_id', 'entry_id'))
  };
}

function entryError(entry) {
  if (!entry.account) return 'Missing account';
  if (!entry.entry_date) return 'Missing entry date';
  if (round2(entry.debit) === 0 && round2(entry.credit) === 0) return 'Debit or credit is required';
  return null;
}

// Validate + coerce a normalized entry coming from the CRUD API.
function validateEntry(body) {
  const entry = {
    entry_date: String(body.entry_date || '').slice(0, 10),
    account: String(body.account || '').trim(),
    account_type: strOrNull(body.account_type),
    description: strOrNull(body.description),
    reference: strOrNull(body.reference),
    debit: round2(num(body.debit)),
    credit: round2(num(body.credit)),
    external_id: strOrNull(body.external_id),
    asset_id: body.asset_id ? Number(body.asset_id) : null
  };
  entry.fiscal_year = yearOf(entry.entry_date);
  const err = entryError(entry);
  if (err) throw Object.assign(new Error(err), { status: 400 });
  return entry;
}

// ---- Write (CRUD) -----------------------------------------------------------

function insertEntry(entry, book, userId, companyId, source) {
  const ts = now();
  const result = run(
    `INSERT INTO gl_entries (entity_id, book_code, entry_date, fiscal_year, account, account_type, description, reference, debit, credit, asset_id, external_id, source, created_by, created_at, updated_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    [
      companyId, book, entry.entry_date, entry.fiscal_year, entry.account, entry.account_type,
      entry.description, entry.reference, entry.debit, entry.credit, entry.asset_id || null,
      entry.external_id, source || 'manual', userId, ts, ts
    ]
  );
  const created = getEntry(result.lastInsertRowid, companyId);
  audit(userId, 'create', 'gl_entry', result.lastInsertRowid, null, created);
  return created;
}

function createEntry(book, body, userId, companyId) {
  const entry = validateEntry(body);
  require('./closePeriods').assertPeriodOpen(companyId, book, entry.entry_date);
  return insertEntry(entry, book, userId, companyId, 'manual');
}

function updateEntry(id, body, userId, companyId) {
  const before = getEntry(id, companyId);
  if (!before) return null;
  const entry = validateEntry({ ...before, ...body });
  // Block edits dated in (or moving into) a closed period for this entry's book.
  require('./closePeriods').assertPeriodOpen(companyId, before.book_code, before.entry_date);
  require('./closePeriods').assertPeriodOpen(companyId, before.book_code, entry.entry_date);
  run(
    `UPDATE gl_entries SET entry_date = ?, fiscal_year = ?, account = ?, account_type = ?, description = ?,
       reference = ?, debit = ?, credit = ?, asset_id = ?, external_id = ?, updated_at = ? WHERE id = ?`,
    [
      entry.entry_date, entry.fiscal_year, entry.account, entry.account_type, entry.description,
      entry.reference, entry.debit, entry.credit, entry.asset_id || null, entry.external_id, now(), id
    ]
  );
  const after = getEntry(id, companyId);
  audit(userId, 'update', 'gl_entry', id, before, after);
  return after;
}

function deleteEntry(id, userId, companyId) {
  const before = getEntry(id, companyId);
  if (!before) return false;
  require('./closePeriods').assertPeriodOpen(companyId, before.book_code, before.entry_date);
  run('DELETE FROM gl_entries WHERE id = ?', [id]);
  audit(userId, 'delete', 'gl_entry', id, before, null);
  return true;
}

// ---- Import (preview + commit) ----------------------------------------------

// Find an existing entry an import row should reconcile with: by external_id first, else the natural
// key (date + account + debit + credit + reference) within the same company + book.
function matchExisting(entry, companyId, book) {
  if (entry.external_id) {
    const byId = one('SELECT * FROM gl_entries WHERE entity_id = ? AND book_code = ? AND external_id = ?', [companyId, book, entry.external_id]);
    if (byId) return byId;
  }
  return one(
    `SELECT * FROM gl_entries
     WHERE entity_id = ? AND book_code = ? AND entry_date = ? AND account = ?
       AND ROUND(debit, 2) = ? AND ROUND(credit, 2) = ? AND IFNULL(reference, '') = IFNULL(?, '')`,
    [companyId, book, entry.entry_date, entry.account, entry.debit, entry.credit, entry.reference]
  );
}

function differs(existing, entry) {
  return existing.entry_date !== entry.entry_date
    || existing.account !== entry.account
    || (existing.account_type || '') !== (entry.account_type || '')
    || (existing.description || '') !== (entry.description || '')
    || (existing.reference || '') !== (entry.reference || '')
    || round2(existing.debit) !== entry.debit
    || round2(existing.credit) !== entry.credit;
}

// Classify each raw import row as new / update / duplicate / error (no DB writes).
function previewImport(rawRows, book, companyId) {
  const summary = { new: 0, update: 0, duplicate: 0, error: 0 };
  const rows = (rawRows || []).map((raw, index) => {
    const entry = normalizeRow(raw);
    const err = entryError(entry);
    if (err) { summary.error += 1; return { index, status: 'error', entry, error: err }; }
    const existing = matchExisting(entry, companyId, book);
    if (!existing) { summary.new += 1; return { index, status: 'new', entry, existing_id: null }; }
    if (differs(existing, entry)) { summary.update += 1; return { index, status: 'update', entry, existing_id: existing.id }; }
    summary.duplicate += 1;
    return { index, status: 'duplicate', entry, existing_id: existing.id };
  });
  return { summary, rows };
}

// Apply classified rows by strategy. Re-validates company/book server-side.
//   skip   — insert only 'new'; conflicts skipped
//   merge  — insert 'new', update 'update'; identical duplicates skipped
//   insert — insert every non-error row as a brand-new entry (ignore matches)
function commitImport(rows, strategy, book, userId, companyId) {
  const result = { inserted: 0, updated: 0, skipped: 0, errors: 0 };
  const mode = ['skip', 'merge', 'insert'].includes(strategy) ? strategy : 'merge';
  tx(() => {
    for (const row of rows || []) {
      const entry = normalizeRow(row.entry || {});
      if (entryError(entry)) { result.errors += 1; continue; }
      const status = row.status;
      if (mode === 'insert') { insertEntry(entry, book, userId, companyId, 'import'); result.inserted += 1; continue; }
      if (status === 'new') { insertEntry(entry, book, userId, companyId, 'import'); result.inserted += 1; continue; }
      if (mode === 'merge' && status === 'update') {
        const existing = row.existing_id ? getEntry(row.existing_id, companyId) : matchExisting(entry, companyId, book);
        if (existing) { updateEntry(existing.id, entry, userId, companyId); result.updated += 1; }
        else { insertEntry(entry, book, userId, companyId, 'import'); result.inserted += 1; }
        continue;
      }
      result.skipped += 1; // duplicates, or conflicts under 'skip'
    }
  });
  audit(userId, 'import', 'gl_entries', book, null, { strategy: mode, ...result });
  return result;
}

// ---- Export -----------------------------------------------------------------

function entriesReport(filters, companyId) {
  const { entries, summary } = listEntries(filters, companyId);
  return {
    title: `${filters.book} general ledger entries${filters.year ? ` — ${filters.year}` : ''}`,
    generatedAt: now(),
    columns: [
      { key: 'entry_date', label: 'Date', format: 'text' },
      { key: 'account', label: 'Account', format: 'text' },
      { key: 'account_type', label: 'Account type', format: 'text' },
      { key: 'description', label: 'Description', format: 'text' },
      { key: 'reference', label: 'Reference', format: 'text' },
      { key: 'debit', label: 'Debit', format: 'currency' },
      { key: 'credit', label: 'Credit', format: 'currency' },
      { key: 'external_id', label: 'External ID', format: 'text' },
      { key: 'source', label: 'Source', format: 'text' }
    ],
    rows: entries,
    totals: { debit: summary.debit, credit: summary.credit },
    meta: { note: `${summary.count} entr${summary.count === 1 ? 'y' : 'ies'} · debit ${summary.debit} · credit ${summary.credit} · balance ${summary.balance}` }
  };
}

// Full change timeline for one entry, drawn from the audit log (create/update events, and any
// import-driven changes — all share entity_type 'gl_entry'). Returns null if the entry isn't in
// this company (so it can't be probed cross-company). Newest first.
function entryHistory(id, companyId) {
  if (!getEntry(id, companyId)) return null;
  return all(
    `SELECT al.id, al.action, al.before_json, al.after_json, al.created_at, u.name AS actor_name
     FROM audit_logs al LEFT JOIN users u ON u.id = al.actor_id
     WHERE al.entity_type = 'gl_entry' AND al.entity_id = ?
     ORDER BY al.id DESC`,
    [String(id)]
  ).map((row) => ({
    id: row.id,
    action: row.action,
    actor_name: row.actor_name,
    created_at: row.created_at,
    before: parseJson(row.before_json, null),
    after: parseJson(row.after_json, null)
  }));
}

module.exports = {
  commitImport,
  createEntry,
  deleteEntry,
  entriesReport,
  entryHistory,
  getEntry,
  insertEntry,
  listEntries,
  previewImport,
  updateEntry
};
