/*
  Service for managing depreciation zones, which are used to define special rules for assets located in certain areas (e.g. opportunity zones). This includes functions for creating, updating, deleting, and retrieving zones, as well as caching for efficient lookups during depreciation calculations.
*/
const moment = require('moment');
const { all, audit, now, one, run } = require('../db');

let cache = null;

// Clear the cache, forcing a reload from the database on the next lookup. This should be called after any changes to the zones to ensure that the cache stays up to date.
function clearCache() {
  console.log(`${moment().format()} 🧹 Clearing depreciation zones cache`);
  cache = null;
}

// Load all zones into a cache object keyed by code for efficient lookups. This is used during depreciation calculations to quickly retrieve zone information without hitting the database repeatedly, improving performance while still ensuring that we have the necessary data available.
function lookup() {
  if (!cache) {
    console.log(`${moment().format()} 📦 Loading depreciation zones into cache`);
    cache = {};
    for (const row of all('SELECT * FROM depreciation_zones')) cache[row.code] = row;
  }
  console.log(`${moment().format()} ✅ Depreciation zones cache loaded with ${Object.keys(cache).length} zones`);
  return cache;
}

// Raw row (incl. dates) for the engine; null when unknown or disabled.
function getZone(code) {
  if (!code) {
    // Note: we allow getZone to be called with a falsy code (e.g. null or undefined) to simplify logic in the depreciation engine, but we return null in that case since a valid zone code is required for a lookup; this allows the engine to call getZone without needing to check for the presence of a code first, while still ensuring that we don't attempt to look up an invalid code.
    console.log(`${moment().format()} ⚠️ No zone code provided for lookup`);
    return null;
  }
  const zone = lookup()[code];
  return zone && zone.enabled ? zone : null;
}

// Helper function to create a slug from a string, used for generating zone codes. This ensures that zone codes are consistent and URL-friendly, while still allowing for human-readable names to be used for the zones.
function slugify(value) {
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40);
}

// Convert a database row into a zone object with the expected properties for the depreciation engine, including an asset count for reporting purposes. This allows us to separate the raw database representation from the format used in the engine, while still providing all the necessary information for both use cases.
function fromRow(row) {
  if (!row) {
    console.log(`${moment().format()} ⚠️ No zone row provided for conversion`);
    return null;
  }
  return {
    id: row.id,
    code: row.code,
    name: row.name,
    allowance_percent: row.allowance_percent,
    pis_start: row.pis_start,
    pis_end: row.pis_end,
    real_property_end: row.real_property_end,
    max_recovery_years: row.max_recovery_years,
    leasehold_life_years: row.leasehold_life_years,
    section_179_increase: row.section_179_increase,
    section_179_cost_factor: row.section_179_cost_factor,
    section_179_threshold_increase: row.section_179_threshold_increase,
    section_179_pis_start: row.section_179_pis_start,
    section_179_pis_end: row.section_179_pis_end,
    notes: row.notes,
    enabled: Boolean(row.enabled),
    asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE special_zone = ?', [row.code]).c
  };
}

// List all zones, converting each database row into a zone object for use in the application. This allows us to retrieve all zones in a format that includes the necessary properties for both the engine and reporting, while still keeping the database representation separate.
function list() {
  console.log(`${moment().format()} 📋 Listing all depreciation zones`);
  return all('SELECT * FROM depreciation_zones ORDER BY name').map(fromRow);
}

// Normalize and validate input data for creating or updating a zone, ensuring that all required fields are present and properly formatted. This helps to prevent invalid data from being entered into the database, while still allowing for flexibility in the input format (e.g. allowing empty strings to be treated as null).
function normalize(body) {
  console.log(`${moment().format()} 🧹 Normalizing input data for zone: ${JSON.stringify(body)}`);
  const num = (v) => (v === '' || v === null || v === undefined ? null : Number(v));
  return {
    name: String(body.name || '').trim(),
    allowance_percent: Number(body.allowance_percent) || 0,
    pis_start: body.pis_start || null,
    pis_end: body.pis_end || null,
    real_property_end: body.real_property_end || null,
    max_recovery_years: num(body.max_recovery_years),
    leasehold_life_years: num(body.leasehold_life_years),
    section_179_increase: Number(body.section_179_increase) || 0,
    section_179_cost_factor: body.section_179_cost_factor === '' || body.section_179_cost_factor === null || body.section_179_cost_factor === undefined ? 1 : Number(body.section_179_cost_factor),
    section_179_threshold_increase: Number(body.section_179_threshold_increase) || 0,
    section_179_pis_start: body.section_179_pis_start || null,
    section_179_pis_end: body.section_179_pis_end || null,
    notes: body.notes || null,
    enabled: body.enabled === false ? 0 : 1
  };
}

// Create a new zone with the specified properties, ensuring that the code is unique and properly formatted. This allows us to add new zones to the system while maintaining data integrity and providing feedback on any issues with the input.
function createZone(body, userId) {
  console.log(`${moment().format()} ➕ Creating new depreciation zone with input: ${JSON.stringify(body)}`);
  const input = normalize(body);
  if (!input.name) {
    console.log(`${moment().format()} ⚠️ Zone name is required for creation`);
    throw Object.assign(new Error('A zone name is required'), { status: 400 });
  }
  const code = slugify(body.code || input.name);
  if (!code) {
    // Note: we require a valid code for the zone, which can be provided directly or generated from the name; if we can't generate a valid code, we throw an error since the code is necessary for lookups and must be unique.
    console.log(`${moment().format()} ⚠️ Zone code is required for creation`);
    throw Object.assign(new Error('A valid zone code is required'), { status: 400 });
  }
  if (one('SELECT id FROM depreciation_zones WHERE code = ?', [code])) {
    // Note: we check for the existence of a zone with the same code before attempting to create a new one, and we throw an error if a duplicate is found since the code must be unique; this allows us to prevent conflicts and maintain data integrity in the database.
    console.log(`${moment().format()} ⚠️ A zone with that code already exists`);
    throw Object.assign(new Error('A zone with that code already exists'), { status: 400 });
  }
  const ts = now();
  // Note: we use a parameterized query here to prevent SQL injection, and we include all the relevant fields in the insert statement to ensure that the new zone is created with the correct properties; this allows us to safely add new zones to the database while maintaining data integrity.
  run(
    `INSERT INTO depreciation_zones (code, name, allowance_percent, pis_start, pis_end, real_property_end, max_recovery_years, leasehold_life_years, section_179_increase, section_179_cost_factor, section_179_threshold_increase, section_179_pis_start, section_179_pis_end, notes, enabled, created_at, updated_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    [code, input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.section_179_increase, input.section_179_cost_factor, input.section_179_threshold_increase, input.section_179_pis_start, input.section_179_pis_end, input.notes, input.enabled, ts, ts]
  );
  // Clear the cache after creating a new zone to ensure that it is included in subsequent lookups, and audit the creation action for tracking purposes; this allows us to maintain an accurate cache and keep a record of changes to the zones for accountability.
  console.log(`${moment().format()} 🧹 Clearing cache after creating new zone: ${code}`);
  clearCache();
  audit(userId, 'create', 'depreciation_zone', code, null, input);
  console.log(`${moment().format()} ✅ Depreciation zone created with code: ${code}`);
  return fromRow(one('SELECT * FROM depreciation_zones WHERE code = ?', [code]));
}

// Update an existing zone with the specified properties, ensuring that the code exists and that the input is properly normalized. This allows us to modify existing zones while maintaining data integrity and providing feedback on any issues with the input.
function updateZone(code, body, userId) {
  console.log(`${moment().format()} ✏️ Updating depreciation zone with code: ${code}, input: ${JSON.stringify(body)}`);
  const before = one('SELECT * FROM depreciation_zones WHERE code = ?', [code]);
  if (!before) {
    // Note: we check for the existence of the zone before attempting to update it, and we return null if it doesn't exist since there's nothing to update; this allows us to handle cases where an invalid code is provided gracefully, while still ensuring that we don't attempt to update a non-existent record.
    console.log(`${moment().format()} ⚠️ Zone with code ${code} not found`);
    return null;
  }
  const input = normalize({ ...before, ...body, enabled: body.enabled === undefined ? Boolean(before.enabled) : body.enabled });
  // Note: we use a parameterized query here to prevent SQL injection, and we include all the relevant fields in the update statement to ensure that the zone is updated with the correct properties; this allows us to safely modify existing zones in the database while maintaining data integrity.
  run(
    `UPDATE depreciation_zones SET name = ?, allowance_percent = ?, pis_start = ?, pis_end = ?, real_property_end = ?,
       max_recovery_years = ?, leasehold_life_years = ?, section_179_increase = ?, section_179_cost_factor = ?,
       section_179_threshold_increase = ?, section_179_pis_start = ?, section_179_pis_end = ?, notes = ?, enabled = ?, updated_at = ? WHERE code = ?`,
    [input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.section_179_increase, input.section_179_cost_factor, input.section_179_threshold_increase, input.section_179_pis_start, input.section_179_pis_end, input.notes, input.enabled, now(), code]
  );
  // Clear the cache after updating the zone to ensure that the changes are reflected in subsequent lookups, and audit the update action for tracking purposes; this allows us to maintain an accurate cache and keep a record of changes to the zones for accountability.
  console.log(`${moment().format()} 🧹 Clearing cache after updating zone: ${code}`);
  clearCache();
  audit(userId, 'update', 'depreciation_zone', code, before, input);
  return fromRow(one('SELECT * FROM depreciation_zones WHERE code = ?', [code]));
}

// Delete a zone by code, ensuring that the code exists before attempting to delete it. This allows us to remove zones from the system while maintaining data integrity and providing feedback on any issues with the input.
function deleteZone(code, userId) {
  console.log(`${moment().format()} 🗑️ Deleting depreciation zone with code: ${code}`);
  const before = one('SELECT * FROM depreciation_zones WHERE code = ?', [code]);
  if (!before) {
    // Note: we check for the existence of the zone before attempting to delete it, and we return false if it doesn't exist since there's nothing to delete; this allows us to handle cases where an invalid code is provided gracefully, while still ensuring that we don't attempt to delete a non-existent record.
    console.log(`${moment().format()} ⚠️ Zone with code ${code} not found`);
    return false;
  }
  console.log(`${moment().format()} 🗑️ Deleting depreciation zone with code: ${code}`);
  run('DELETE FROM depreciation_zones WHERE code = ?', [code]);
  // Clear the cache after deleting the zone to ensure that it is removed from subsequent lookups, and audit the deletion action for tracking purposes; this allows us to maintain an accurate cache and keep a record of changes to the zones for accountability.
  console.log(`${moment().format()} 🧹 Clearing cache after deleting zone: ${code}`);
  clearCache();
  audit(userId, 'delete', 'depreciation_zone', code, before, null);
  return true;
}

// Export the functions for use in other modules. This allows the zone management logic to be organized in a single module and reused across different parts of the application, such as in API endpoints or the depreciation engine.
module.exports = {
  clearCache,
  createZone,
  deleteZone,
  getZone,
  list,
  updateZone
};
