const { parseJson } = require('./db');

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

function yearFromDate(value) {
  if (!value) return new Date().getFullYear();
  return new Date(`${value}T00:00:00`).getFullYear();
}

function monthFromDate(value) {
  if (!value) return 1;
  return new Date(`${value}T00:00:00`).getMonth() + 1;
}

function parseMaybeJson(value, fallback = null) {
  if (typeof value === 'string') return parseJson(value, fallback);
  return value ?? fallback;
}

function totalDepreciableBasis(asset, book) {
  const cost = Number(book.cost ?? asset.acquisition_cost ?? 0);
  const land = Number(asset.land_value || 0);
  const salvage = Number(asset.salvage_value || 0);
  const businessUse = Number(book.business_use_percent ?? asset.business_use_percent ?? 100) / 100;
  return Math.max(0, (cost - land - salvage) * businessUse);
}

function section179Amount(asset, book) {
  return Math.min(totalDepreciableBasis(asset, book), Number(book.section_179_amount || 0));
}

function bonusAmount(asset, book) {
  const after179 = Math.max(0, totalDepreciableBasis(asset, book) - section179Amount(asset, book));
  return after179 * (Number(book.bonus_percent || 0) / 100);
}

function depreciableBasis(asset, book) {
  return Math.max(0, totalDepreciableBasis(asset, book) - section179Amount(asset, book) - bonusAmount(asset, book));
}

function conventionFor(book, methodRule) {
  return methodRule.defaultConvention || book.convention || 'none';
}

function yearFraction(index, convention, asset) {
  if (index < 0) return 0;
  const month = monthFromDate(asset.in_service_date || asset.acquired_date);
  const quarter = Math.ceil(month / 3);

  if (convention === 'none') return 1;
  if (convention === 'full_month') {
    const first = (13 - month) / 12;
    if (index === 0) return first;
    return null;
  }
  if (convention === 'mid_month') {
    const first = (12 - month + 0.5) / 12;
    if (index === 0) return first;
    return null;
  }
  if (convention === 'mid_quarter') {
    const firstByQuarter = { 1: 0.875, 2: 0.625, 3: 0.375, 4: 0.125 };
    if (index === 0) return firstByQuarter[quarter] || 0.5;
    return null;
  }
  if (convention === 'half_year') {
    if (index === 0) return 0.5;
    return null;
  }
  return 1;
}

function fractionForIndex(index, convention, asset, totalYears) {
  const first = yearFraction(0, convention, asset);
  const explicit = yearFraction(index, convention, asset);
  if (explicit !== null) return explicit;
  const finalIndex = Math.ceil(totalYears + (1 - first)) - 1;
  if (index > finalIndex) return 0;
  if (index === finalIndex) return Math.max(0, 1 - first);
  return 1;
}

function straightLine(asset, book, index, totalYears, methodRule) {
  const basis = depreciableBasis(asset, book);
  return (basis / totalYears) * fractionForIndex(index, conventionFor(book, methodRule), asset, totalYears);
}

function sumOfYearsDigits(asset, book, index, totalYears) {
  const basis = depreciableBasis(asset, book);
  const denominator = (totalYears * (totalYears + 1)) / 2;
  return basis * ((totalYears - index) / denominator);
}

function decliningBalance(asset, book, index, totalYears, multiplier, methodRule) {
  const basis = depreciableBasis(asset, book);
  let bookValue = basis;
  let elapsed = 0;
  const convention = conventionFor(book, methodRule);

  for (let i = 0; i <= index; i += 1) {
    const fraction = fractionForIndex(i, convention, asset, totalYears);
    if (fraction <= 0 || bookValue <= 0) return 0;

    const dbAmount = bookValue * (multiplier / totalYears) * fraction;
    const remainingLife = Math.max(0.0001, totalYears - elapsed);
    const slAmount = (bookValue / remainingLife) * fraction;
    const selected = methodRule.switchToStraightLine ? Math.max(dbAmount, slAmount) : dbAmount;
    const amount = Math.min(bookValue, selected);

    if (i === index) return amount;
    bookValue -= amount;
    elapsed += fraction;
  }
  return 0;
}

function rateTable(asset, book, index, methodRule) {
  const basis = depreciableBasis(asset, book);
  const rates = methodRule.rates || [];
  if (!rates.length && methodRule.fallbackFormula === 'straight_line') {
    return straightLine(asset, book, index, Math.max(1, Math.ceil((methodRule.lifeMonths || book.useful_life_months || 60) / 12)), methodRule);
  }
  return basis * Number(rates[index] || 0);
}

function getMethodRule(ruleSet, code) {
  const rules = typeof ruleSet === 'string' ? parseJson(ruleSet, {}) : ruleSet || {};
  return (rules.methods || []).find((method) => method.code === code) || {
    code,
    formula: code,
    label: code
  };
}

function annualSchedule(asset, book, ruleSet, options = {}) {
  const methodRule = getMethodRule(ruleSet, book.depreciation_method || 'straight_line');
  const usefulLifeMonths = Number(methodRule.lifeMonths || book.useful_life_months || asset.useful_life_months || 60);
  const totalYears = Math.max(1, Math.ceil(usefulLifeMonths / 12));
  const placedInServiceYear = yearFromDate(asset.in_service_date || asset.acquired_date);
  const startYear = Number(options.startYear || placedInServiceYear);
  const endYear = Number(options.endYear || startYear + totalYears + 1);
  const manual = parseMaybeJson(book.manual_depreciation, {});
  const rows = [];
  let accumulated = Number(asset.prior_depreciation || 0);

  for (let year = placedInServiceYear; year <= endYear; year += 1) {
    const index = year - placedInServiceYear;
    let depreciation = 0;
    if (index >= 0) {
      if (index === 0) {
        depreciation += section179Amount(asset, book) + bonusAmount(asset, book);
      }

      if (methodRule.formula === 'rate_table') {
        depreciation += rateTable(asset, book, index, methodRule);
      } else if (methodRule.formula === 'sum_of_years_digits') {
        depreciation += sumOfYearsDigits(asset, book, index, totalYears);
      } else if (methodRule.formula === 'declining_balance' || methodRule.formula === 'macrs_declining_balance') {
        depreciation += decliningBalance(asset, book, index, totalYears, Number(methodRule.rateMultiplier || 2), methodRule);
      } else if (methodRule.formula === 'acrs_alternate_straight_line') {
        depreciation += straightLine(asset, book, index, totalYears, { ...methodRule, defaultConvention: 'half_year' });
      } else if (methodRule.formula === 'manual') {
        depreciation = Number(manual[year] || 0);
      } else {
        depreciation += straightLine(asset, book, index, totalYears, methodRule);
      }
    }

    const basis = totalDepreciableBasis(asset, book);
    depreciation = Math.max(0, Math.min(depreciation, Math.max(0, basis - accumulated)));
    accumulated += depreciation;

    if (year >= startYear) {
      rows.push({
        year,
        book: book.book_type,
        method: methodRule.code,
        methodLabel: methodRule.label || methodRule.code,
        depreciation: money(depreciation),
        accumulated: money(accumulated),
        netBookValue: money(Math.max(0, Number(book.cost ?? asset.acquisition_cost ?? 0) - accumulated))
      });
    }
  }

  return rows;
}

function monthlyFromAnnual(row) {
  const monthly = money(row.depreciation / 12);
  return Array.from({ length: 12 }, (_, index) => ({
    month: index + 1,
    year: row.year,
    book: row.book,
    depreciation: monthly
  }));
}

module.exports = {
  annualSchedule,
  depreciableBasis,
  totalDepreciableBasis,
  getMethodRule,
  monthlyFromAnnual,
  money
};
