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

const assetColumns = [
  'asset_id',
  'entity_id',
  'template_id',
  'description',
  'category',
  'status',
  'acquisition_cost',
  'acquired_date',
  'in_service_date',
  'useful_life_months',
  'salvage_value',
  'land_value',
  'prior_depreciation',
  'location',
  'department',
  'group_name',
  'custodian',
  'vendor',
  'serial_number',
  'invoice_number',
  'gl_asset_account',
  'gl_expense_account',
  'gl_accumulated_account',
  'barcode_value',
  'condition',
  'new_or_used',
  'listed_property',
  'passenger_auto',
  'business_use_percent',
  'investment_use_percent',
  'disposal_date',
  'disposal_price',
  'disposal_expense',
  'disposal_type',
  'property_type',
  'special_zone',
  'notes',
  'custom_fields'
];

const defaultBooks = ['GAAP', 'FEDERAL', 'STATE', 'AMT', 'ACE', 'USER'];

// Depreciation books currently enabled in the registry (excludes the maintenance/PM book).
function enabledBookCodes(companyId) {
  try {
    const rows = all(
      "SELECT code FROM books WHERE enabled = 1 AND book_type != 'maintenance' AND (? IS NULL OR entity_id = ?) ORDER BY sort_order, id",
      [companyId || null, companyId || null]
    );
    if (rows.length) return rows.map((row) => row.code);
  } catch {
    // books table may not exist on a very old database
  }
  return defaultBooks;
}

function assetFromRow(row) {
  if (!row) return null;
  return {
    ...row,
    listed_property: Boolean(row.listed_property),
    passenger_auto: Boolean(row.passenger_auto),
    custom_fields: parseJson(row.custom_fields, {}),
    books: row.books ? parseJson(row.books, []) : undefined
  };
}

function assetPhotos(id) {
  return all('SELECT id, name, mime_type, caption, data_base64 FROM asset_photos WHERE asset_id = ? ORDER BY sort_order, id', [id])
    .map((p) => ({ id: p.id, name: p.name, mime: p.mime_type, caption: p.caption, data: p.data_base64 }));
}

function addAssetPhoto(assetId, body, userId) {
  if (!one('SELECT id FROM assets WHERE id = ?', [assetId])) return null;
  if (!body.data) throw Object.assign(new Error('Photo data is required'), { status: 400 });
  const result = run(
    'INSERT INTO asset_photos (asset_id, name, mime_type, caption, data_base64, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
    [assetId, body.name || null, body.mime || body.mime_type || 'image/jpeg', body.caption || null, body.data, userId, now()]
  );
  audit(userId, 'create', 'asset_photo', result.lastInsertRowid, null, { asset_id: assetId, name: body.name });
  return one('SELECT id, name, mime_type, caption FROM asset_photos WHERE id = ?', [result.lastInsertRowid]);
}

function updateAssetPhoto(assetId, photoId, body, userId) {
  const result = run('UPDATE asset_photos SET caption = ? WHERE id = ? AND asset_id = ?', [body.caption || null, photoId, assetId]);
  if (!result.changes) return null;
  audit(userId, 'update', 'asset_photo', photoId, null, { asset_id: assetId });
  return one('SELECT id, name, mime_type, caption FROM asset_photos WHERE id = ?', [photoId]);
}

function deleteAssetPhoto(assetId, photoId, userId) {
  const result = run('DELETE FROM asset_photos WHERE id = ? AND asset_id = ?', [photoId, assetId]);
  if (!result.changes) return false;
  audit(userId, 'delete', 'asset_photo', photoId, null, { asset_id: assetId });
  return true;
}

function hydrateAsset(id) {
  const asset = assetFromRow(one('SELECT * FROM assets WHERE id = ?', [id]));
  if (!asset) return null;
  asset.books = all('SELECT * FROM asset_books WHERE asset_id = ? ORDER BY book_type', [id]).map((book) => ({
    ...book,
    active: Boolean(book.active),
    manual_depreciation: parseJson(book.manual_depreciation, {})
  }));
  asset.leases = all('SELECT * FROM leases WHERE asset_id = ? ORDER BY start_date DESC', [id]);
  asset.disposals = all('SELECT * FROM disposals WHERE asset_id = ? ORDER BY disposal_date DESC, id DESC', [id]);
  asset.adjustments = all('SELECT * FROM asset_adjustments WHERE asset_id = ? ORDER BY adjustment_date DESC, id DESC', [id]);
  asset.pm = assetPmRows(id);
  asset.tasks = all('SELECT * FROM tasks WHERE asset_id = ? ORDER BY due_date IS NULL, due_date', [id]);
  asset.warranties = all('SELECT id, provider, phone, start_date, expiration_date, warranty_limit, actual_usage, coverage_details, document_name, created_at FROM warranties WHERE asset_id = ?', [id]);
  asset.files = all('SELECT id, file_name, mime_type, size, uploaded_at FROM asset_files WHERE asset_id = ?', [id]);
  asset.photos = assetPhotos(id);
  asset.notes_log = all(
    `SELECT n.id, n.body, n.created_at, u.name AS author
     FROM asset_notes n
     LEFT JOIN users u ON u.id = n.created_by
     WHERE n.asset_id = ?
     ORDER BY n.id DESC`,
    [id]
  );
  return asset;
}

function lookupAssetByCode(code, companyId) {
  const value = String(code || '').trim();
  if (!value) return null;
  const row = one(
    `SELECT id FROM assets
     WHERE (? IS NULL OR entity_id = ?)
       AND (barcode_value = ? COLLATE NOCASE
        OR asset_id = ? COLLATE NOCASE
        OR serial_number = ? COLLATE NOCASE)
     ORDER BY (barcode_value = ? COLLATE NOCASE) DESC, id
     LIMIT 1`,
    [companyId || null, companyId || null, value, value, value, value]
  );
  return row ? hydrateAsset(row.id) : null;
}

function nextAssetId() {
  const latest = one("SELECT asset_id FROM assets WHERE asset_id LIKE 'MA-%' ORDER BY id DESC LIMIT 1");
  const number = latest ? Number(String(latest.asset_id).replace('MA-', '')) + 1 : 1;
  return `MA-${String(number).padStart(5, '0')}`;
}

// Replace the first run of '#' in a pattern with the zero-padded number.
// e.g. formatAssetId('DT-######', 42) -> 'DT-000042'
function formatAssetId(pattern, n) {
  return String(pattern).replace(/#+/, (hashes) => String(n).padStart(hashes.length, '0'));
}

// If the asset's category has an assigned ID template, generate the next unique tag from it
// and advance the template's counter. Returns null when the category has no template.
function nextAssetIdFromCategory(categoryName) {
  if (!categoryName) return null;
  let templateId = null;
  try {
    const category = one("SELECT metadata FROM reference_values WHERE type = 'category' AND name = ?", [categoryName]);
    templateId = category ? parseJson(category.metadata, {}).id_template_id : null;
  } catch {
    return null;
  }
  if (!templateId) return null;
  const template = one('SELECT * FROM asset_id_templates WHERE id = ?', [templateId]);
  if (!template || !/#/.test(template.pattern || '')) return null;
  let n = Number(template.next_number) || 1;
  let candidate = formatAssetId(template.pattern, n);
  while (one('SELECT id FROM assets WHERE asset_id = ?', [candidate])) {
    n += 1;
    candidate = formatAssetId(template.pattern, n);
  }
  run('UPDATE asset_id_templates SET next_number = ?, updated_at = ? WHERE id = ?', [n + 1, now(), templateId]);
  return candidate;
}

// Resolve the asset tag for a new asset: an explicit value wins; otherwise use the
// category's ID template, falling back to the default MA-##### sequence.
function resolveNewAssetId(input) {
  if (input.asset_id) return input.asset_id;
  return nextAssetIdFromCategory(input.category) || nextAssetId();
}

// The Asset Class Number is a property of the category — every asset in the category shares it.
function assetClassNumberForCategory(categoryName) {
  if (!categoryName) return null;
  try {
    const category = one("SELECT metadata FROM reference_values WHERE type = 'category' AND name = ?", [categoryName]);
    return category ? (parseJson(category.metadata, {}).asset_class_number || null) : null;
  } catch {
    return null;
  }
}

function normalizeAssetInput(body) {
  const input = {};
  for (const column of assetColumns) {
    if (Object.prototype.hasOwnProperty.call(body, column)) input[column] = body[column];
  }
  input.description = input.description || body.name || 'Untitled asset';
  input.category = input.category || 'Uncategorized';
  input.status = input.status || 'in_service';
  input.asset_id = input.asset_id || null; // resolved by the caller (template or default sequence)
  input.entity_id = input.entity_id || one('SELECT id FROM entities ORDER BY id LIMIT 1')?.id;
  input.acquisition_cost = Number(input.acquisition_cost || 0);
  input.useful_life_months = Number(input.useful_life_months || 60);
  input.salvage_value = Number(input.salvage_value || 0);
  input.land_value = Number(input.land_value || 0);
  input.prior_depreciation = Number(input.prior_depreciation || 0);
  input.business_use_percent = Number(input.business_use_percent || 100);
  input.investment_use_percent = Number(input.investment_use_percent || 0);
  input.custom_fields = json(input.custom_fields || {});
  input.listed_property = input.listed_property ? 1 : 0;
  input.passenger_auto = input.passenger_auto ? 1 : 0;
  return input;
}

function createBooks(assetId, asset, books = [], companyId) {
  const selectedBooks = books.length ? books : enabledBookCodes(companyId).map((book) => ({ book_type: book }));
  for (const book of selectedBooks) {
    run(
      `INSERT OR REPLACE INTO asset_books (
        asset_id, book_type, active, cost, depreciation_method, convention, useful_life_months,
        section_179_amount, bonus_percent, business_use_percent, investment_use_percent, manual_depreciation
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        assetId,
        book.book_type || book.book || 'GAAP',
        book.active === false ? 0 : 1,
        book.cost ?? asset.acquisition_cost,
        book.depreciation_method || asset.depreciation_method || 'straight_line',
        book.convention || asset.convention || 'half_year',
        book.useful_life_months || asset.useful_life_months,
        Number(book.section_179_amount || 0),
        Number(book.bonus_percent || 0),
        book.business_use_percent ?? asset.business_use_percent ?? 100,
        book.investment_use_percent ?? asset.investment_use_percent ?? 0,
        json(book.manual_depreciation || {})
      ]
    );
  }
}

function listAssets(query = {}) {
  return all(`
    SELECT a.*, e.name AS entity_name
    FROM assets a
    LEFT JOIN entities e ON e.id = a.entity_id
    WHERE (? IS NULL OR a.status = ?)
      AND (? IS NULL OR a.entity_id = ?)
      AND (? IS NULL OR a.category = ?)
      AND (? IS NULL OR a.description LIKE '%' || ? || '%' OR a.asset_id LIKE '%' || ? || '%')
    ORDER BY a.updated_at DESC
    LIMIT 500
  `, [
    query.status || null,
    query.status || null,
    query.entity_id || null,
    query.entity_id || null,
    query.category || null,
    query.category || null,
    query.search || null,
    query.search || null,
    query.search || null
  ]).map(assetFromRow);
}

function createAsset(body, userId, companyId) {
  require('./midQuarter').clearCache(); // asset set changed → recompute the mid-quarter determination
  const created = now();
  const input = normalizeAssetInput(body);
  if (companyId) input.entity_id = companyId; // new assets belong to the active company
  // A new capitalization can't land in a closed period (gated by the primary book's locks).
  require('./closePeriods').assertSubledgerOpen(input.entity_id, input.in_service_date || input.acquired_date);
  const result = tx(() => {
    input.asset_id = resolveNewAssetId(input); // category ID template, else default sequence
    const insert = run(
      `INSERT INTO assets (${assetColumns.join(', ')}, created_by, created_at, updated_at)
       VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
      [...assetColumns.map((column) => input[column] ?? null), userId, created, created]
    );
    createBooks(insert.lastInsertRowid, input, body.books || [], input.entity_id);
    run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), insert.lastInsertRowid]);
    return insert;
  });
  const asset = hydrateAsset(result.lastInsertRowid);
  audit(userId, 'create', 'asset', asset.id, null, asset);
  return asset;
}

function updateAsset(id, body, userId, companyId) {
  require('./midQuarter').clearCache();
  const before = hydrateAsset(id);
  if (!before) return null;
  if (companyId && before.entity_id !== companyId) return null; // belongs to another company

  const input = normalizeAssetInput({ ...before, ...body });
  if (companyId) input.entity_id = companyId; // assets stay within their company in phase 1
  // Block changes to the financial dates/cost that would fall in a closed period (either the old or the
  // new value), so a locked period's subledger can't be altered. Other field edits are unaffected.
  const financialChanged = ['in_service_date', 'acquired_date', 'acquisition_cost'].some((f) => String(before[f] ?? '') !== String(input[f] ?? ''));
  if (financialChanged) {
    const guard = require('./closePeriods');
    guard.assertSubledgerOpen(input.entity_id, before.in_service_date || before.acquired_date);
    guard.assertSubledgerOpen(input.entity_id, input.in_service_date || input.acquired_date);
  }
  // When depreciation-critical fields change, the caller chooses when the change applies.
  // 'placed_in_service' rebuilds the whole schedule by clearing prior depreciation;
  // 'going_forward' keeps depreciation already recorded (the stored prior depreciation).
  if (body.depreciation_apply === 'placed_in_service') input.prior_depreciation = 0;
  const updated = now();
  tx(() => {
    run(
      `UPDATE assets SET ${assetColumns.map((column) => `${column} = ?`).join(', ')}, updated_at = ? WHERE id = ?`,
      [...assetColumns.map((column) => input[column] ?? null), updated, id]
    );
    if (Array.isArray(body.books)) createBooks(id, input, body.books, input.entity_id);
    run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), id]);
  });

  const asset = hydrateAsset(id);
  audit(userId, 'update', 'asset', asset.id, before, asset);
  return asset;
}

function deleteAsset(id, userId, companyId) {
  require('./midQuarter').clearCache();
  const before = hydrateAsset(id);
  if (!before) return false;
  if (companyId && before.entity_id !== companyId) return false; // belongs to another company
  run('DELETE FROM assets WHERE id = ?', [id]);
  audit(userId, 'delete', 'asset', id, before, null);
  return true;
}

function massUpdateAssets(ids, changes, userId, companyId) {
  require('./midQuarter').clearCache();
  const allowed = ['status', 'location', 'department', 'group_name', 'in_service_date', 'useful_life_months', 'condition', 'disposal_date'];
  const columns = allowed.filter((column) => Object.prototype.hasOwnProperty.call(changes, column));
  // Restrict to assets in the active company so a forged id list can't touch another company's data.
  if (companyId && ids.length) {
    const placeholders = ids.map(() => '?').join(',');
    ids = all(`SELECT id FROM assets WHERE entity_id = ? AND id IN (${placeholders})`, [companyId, ...ids]).map((r) => r.id);
  }
  if (!ids.length || !columns.length) return 0;

  tx(() => {
    for (const id of ids) {
      run(
        `UPDATE assets SET ${columns.map((column) => `${column} = ?`).join(', ')}, updated_at = ? WHERE id = ?`,
        [...columns.map((column) => changes[column]), now(), id]
      );
    }
  });
  audit(userId, 'mass_update', 'asset', ids.join(','), null, { ids, changes });
  return ids.length;
}

function assetExportRows(companyId) {
  return all(`
    SELECT a.*, e.name AS entity_name
    FROM assets a
    LEFT JOIN entities e ON e.id = a.entity_id
    WHERE (? IS NULL OR a.entity_id = ?)
    ORDER BY a.asset_id
  `, [companyId || null, companyId || null]).map((asset) => ({
    ...asset,
    listed_property: Boolean(asset.listed_property),
    custom_fields: parseJson(asset.custom_fields, {})
  }));
}

function upsertImportedAssets(rows, userId, companyId) {
  let imported = 0;
  tx(() => {
    for (const row of rows) {
      const input = normalizeAssetInput(row);
      if (companyId) input.entity_id = companyId; // imports land in the active company
      if (!input.asset_id) input.asset_id = resolveNewAssetId(input);
      // Match within the active company so imports don't collide with another company's asset ids.
      const existing = one('SELECT id FROM assets WHERE asset_id = ? AND (? IS NULL OR entity_id = ?)', [input.asset_id, companyId || null, companyId || null]);
      const classNumber = assetClassNumberForCategory(input.category);
      if (existing) {
        run(
          `UPDATE assets SET ${assetColumns.map((column) => `${column} = ?`).join(', ')}, asset_class_number = ?, updated_at = ? WHERE id = ?`,
          [...assetColumns.map((column) => input[column] ?? null), classNumber, now(), existing.id]
        );
      } else {
        const result = run(
          `INSERT INTO assets (${assetColumns.join(', ')}, created_by, created_at, updated_at)
           VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
          [...assetColumns.map((column) => input[column] ?? null), userId, now(), now()]
        );
        createBooks(result.lastInsertRowid, input, [], input.entity_id);
        run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [classNumber, result.lastInsertRowid]);
      }
      imported += 1;
    }
  });
  return imported;
}

module.exports = {
  addAssetPhoto,
  assetColumns,
  assetExportRows,
  assetFromRow,
  createAsset,
  createBooks,
  defaultBooks,
  deleteAsset,
  deleteAssetPhoto,
  formatAssetId,
  hydrateAsset,
  updateAssetPhoto,
  listAssets,
  lookupAssetByCode,
  massUpdateAssets,
  normalizeAssetInput,
  upsertImportedAssets,
  updateAsset
};
