class Budget < ApplicationRecord
  include Monetizable

  belongs_to :family

  has_many :budget_categories, dependent: :destroy

  validates :start_date, :end_date, presence: true
  validates :start_date, :end_date, uniqueness: { scope: :family_id }

  monetize :budgeted_spending, :expected_income, :allocated_spending,
           :actual_spending, :available_to_spend, :available_to_allocate,
           :estimated_spending, :estimated_income, :actual_income, :remaining_expected_income

  class << self
    def for_date(date)
      find_by(start_date: date.beginning_of_month, end_date: date.end_of_month)
    end

    def find_or_bootstrap(family, date: Date.current)
      Budget.transaction do
        budget = Budget.find_or_create_by!(
          family: family,
          start_date: date.beginning_of_month,
          end_date: date.end_of_month
        ) do |b|
          b.currency = family.currency
        end

        budget.sync_budget_categories

        budget
      end
    end
  end

  def sync_budget_categories
    family.categories.expenses.each do |category|
      budget_categories.find_or_create_by(
        category: category,
      ) do |bc|
        bc.budgeted_spending = 0
        bc.currency = family.currency
      end
    end
  end

  def uncategorized_budget_category
    budget_categories.uncategorized.tap do |bc|
      bc.budgeted_spending = [ available_to_allocate, 0 ].max
      bc.currency = family.currency
    end
  end

  def entries
    family.entries.incomes_and_expenses.where(date: start_date..end_date)
  end

  def name
    start_date.strftime("%B %Y")
  end

  def initialized?
    budgeted_spending.present?
  end

  def income_categories_with_totals
    family.income_categories_with_totals(date: start_date)
  end

  def expense_categories_with_totals
    family.expense_categories_with_totals(date: start_date)
  end

  def current?
    start_date == Date.today.beginning_of_month && end_date == Date.today.end_of_month
  end

  def previous_budget
    prev_month_end_date = end_date - 1.month
    return nil if prev_month_end_date < family.oldest_entry_date
    family.budgets.find_or_bootstrap(family, date: prev_month_end_date)
  end

  def next_budget
    return nil if current?
    next_start_date = start_date + 1.month
    family.budgets.find_or_bootstrap(family, date: next_start_date)
  end

  def to_donut_segments_json
    unused_segment_id = "unused"

    # Continuous gray segment for empty budgets
    return [ { color: "#F0F0F0", amount: 1, id: unused_segment_id } ] unless allocations_valid?

    segments = budget_categories.includes(:category).map do |bc|
      { color: bc.category.color, amount: bc.actual_spending, id: bc.id }
    end

    if available_to_spend.positive?
      segments.push({ color: "#F0F0F0", amount: available_to_spend, id: unused_segment_id })
    end

    segments
  end

  # =============================================================================
  # Actuals: How much user has spent on each budget category
  # =============================================================================
  def estimated_spending
    family.budgeting_stats.avg_monthly_expenses&.abs
  end

  def actual_spending
    expense_categories_with_totals.total_money.amount
  end

  def available_to_spend
    (budgeted_spending || 0) - actual_spending
  end

  def percent_of_budget_spent
    return 0 unless budgeted_spending > 0

    (actual_spending / budgeted_spending.to_f) * 100
  end

  def overage_percent
    return 0 unless available_to_spend.negative?

    available_to_spend.abs / actual_spending.to_f * 100
  end

  # =============================================================================
  # Budget allocations: How much user has budgeted for all categories combined
  # =============================================================================
  def allocated_spending
    budget_categories.sum(:budgeted_spending)
  end

  def allocated_percent
    return 0 unless budgeted_spending && budgeted_spending > 0

    (allocated_spending / budgeted_spending.to_f) * 100
  end

  def available_to_allocate
    (budgeted_spending || 0) - allocated_spending
  end

  def allocations_valid?
    initialized? && available_to_allocate.positive? && allocated_spending > 0
  end

  # =============================================================================
  # Income: How much user earned relative to what they expected to earn
  # =============================================================================
  def estimated_income
    family.budgeting_stats.avg_monthly_income&.abs
  end

  def actual_income
    family.entries.incomes.where(date: start_date..end_date).sum(:amount).abs
  end

  def actual_income_percent
    return 0 unless expected_income > 0

    (actual_income / expected_income.to_f) * 100
  end

  def remaining_expected_income
    expected_income - actual_income
  end

  def surplus_percent
    return 0 unless remaining_expected_income.negative?

    remaining_expected_income.abs / expected_income.to_f * 100
  end
end
