defmodule Mv.MembershipFees.CalendarCycles do
  @moduledoc """
  Calendar-based cycle calculation functions for membership fees.

  This module provides functions for calculating cycle boundaries
  based on interval types (monthly, quarterly, half-yearly, yearly).

  The calculation functions (`calculate_cycle_start/3`, `calculate_cycle_end/2`,
  `next_cycle_start/2`) are pure functions with no side effects.

  The time-dependent functions (`current_cycle?/3`, `last_completed_cycle?/3`)
  depend on a date parameter for testability. Their 2-argument variants
  (`current_cycle?/2`, `last_completed_cycle?/2`) use `Date.utc_today()` and
  are not referentially transparent.

  ## Interval Types

  - `:monthly` - Cycles from 1st to last day of each month
  - `:quarterly` - Cycles from 1st of Jan/Apr/Jul/Oct to last day of quarter
  - `:half_yearly` - Cycles from 1st of Jan/Jul to last day of half-year
  - `:yearly` - Cycles from Jan 1st to Dec 31st

  ## Examples

      iex> date = ~D[2024-03-15]
      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(date, :monthly)
      ~D[2024-03-01]

      iex> cycle_start = ~D[2024-01-01]
      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle_start, :yearly)
      ~D[2024-12-31]

      iex> cycle_start = ~D[2024-01-01]
      iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(cycle_start, :yearly)
      ~D[2025-01-01]
  """

  @typedoc """
  Interval type for membership fee cycles.

  - `:monthly` - Monthly cycles (1st to last day of month)
  - `:quarterly` - Quarterly cycles (1st of quarter to last day of quarter)
  - `:half_yearly` - Half-yearly cycles (1st of half-year to last day of half-year)
  - `:yearly` - Yearly cycles (Jan 1st to Dec 31st)
  """
  @type interval :: :monthly | :quarterly | :half_yearly | :yearly

  @doc """
  Calculates the start date of the cycle that contains the reference date.

  ## Parameters

    - `date` - Ignored in this 3-argument version (kept for API consistency)
    - `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
    - `reference_date` - The date used to determine which cycle to calculate

  ## Returns

    The start date of the cycle containing the reference date.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly, ~D[2024-05-20])
      ~D[2024-05-01]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :quarterly, ~D[2024-05-20])
      ~D[2024-04-01]
  """
  @spec calculate_cycle_start(Date.t(), interval(), Date.t()) :: Date.t()
  def calculate_cycle_start(_date, interval, reference_date) do
    case interval do
      :monthly -> monthly_cycle_start(reference_date)
      :quarterly -> quarterly_cycle_start(reference_date)
      :half_yearly -> half_yearly_cycle_start(reference_date)
      :yearly -> yearly_cycle_start(reference_date)
    end
  end

  @doc """
  Calculates the start date of the cycle that contains the given date.

  This is a convenience function that calls `calculate_cycle_start/3` with `date` as both
  the input and reference date.

  ## Parameters

    - `date` - The date used to determine which cycle to calculate
    - `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)

  ## Returns

    The start date of the cycle containing the given date.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly)
      ~D[2024-03-01]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly)
      ~D[2024-04-01]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly)
      ~D[2024-07-01]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-12-15], :yearly)
      ~D[2024-01-01]
  """
  @spec calculate_cycle_start(Date.t(), interval()) :: Date.t()
  def calculate_cycle_start(date, interval) do
    calculate_cycle_start(date, interval, date)
  end

  @doc """
  Calculates the end date of a cycle based on its start date and interval.

  ## Parameters

    - `cycle_start` - The start date of the cycle
    - `interval` - The interval type

  ## Returns

    The end date of the cycle.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly)
      ~D[2024-03-31]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly)
      ~D[2024-02-29]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly)
      ~D[2024-03-31]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly)
      ~D[2024-06-30]

      iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly)
      ~D[2024-12-31]
  """
  @spec calculate_cycle_end(Date.t(), interval()) :: Date.t()
  def calculate_cycle_end(cycle_start, interval) do
    case interval do
      :monthly -> monthly_cycle_end(cycle_start)
      :quarterly -> quarterly_cycle_end(cycle_start)
      :half_yearly -> half_yearly_cycle_end(cycle_start)
      :yearly -> yearly_cycle_end(cycle_start)
    end
  end

  @doc """
  Calculates the start date of the next cycle.

  ## Parameters

    - `cycle_start` - The start date of the current cycle
    - `interval` - The interval type

  ## Returns

    The start date of the next cycle.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :monthly)
      ~D[2024-02-01]

      iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :quarterly)
      ~D[2024-04-01]

      iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :half_yearly)
      ~D[2024-07-01]

      iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly)
      ~D[2025-01-01]
  """
  @spec next_cycle_start(Date.t(), interval()) :: Date.t()
  def next_cycle_start(cycle_start, interval) do
    cycle_end = calculate_cycle_end(cycle_start, interval)
    next_date = Date.add(cycle_end, 1)
    calculate_cycle_start(next_date, interval)
  end

  @doc """
  Checks if the cycle contains the given date.

  ## Parameters

    - `cycle_start` - The start date of the cycle
    - `interval` - The interval type
    - `today` - The date to check (defaults to today's date)

  ## Returns

    `true` if the given date is within the cycle, `false` otherwise.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-15])
      true

      iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-02-01], :monthly, ~D[2024-03-15])
      false

      iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-01])
      true

      iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-31])
      true
  """
  @spec current_cycle?(Date.t(), interval(), Date.t()) :: boolean()
  def current_cycle?(cycle_start, interval, today) do
    cycle_end = calculate_cycle_end(cycle_start, interval)

    Date.compare(cycle_start, today) in [:lt, :eq] and
      Date.compare(today, cycle_end) in [:lt, :eq]
  end

  @spec current_cycle?(Date.t(), interval()) :: boolean()
  def current_cycle?(cycle_start, interval) do
    current_cycle?(cycle_start, interval, Date.utc_today())
  end

  @doc """
  Checks if the cycle is the last completed cycle.

  A cycle is considered the last completed cycle if:
  - The cycle has ended (cycle_end < today)
  - The next cycle has not ended yet (today <= next_end)

  In other words: `cycle_end < today <= next_end`

  ## Parameters

    - `cycle_start` - The start date of the cycle
    - `interval` - The interval type
    - `today` - The date to check against (defaults to today's date)

  ## Returns

    `true` if the cycle is the last completed cycle, `false` otherwise.

  ## Examples

      iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-03-01], :monthly, ~D[2024-04-01])
      true

      iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-15])
      false

      iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-02-01], :monthly, ~D[2024-04-15])
      false
  """
  @spec last_completed_cycle?(Date.t(), interval(), Date.t()) :: boolean()
  def last_completed_cycle?(cycle_start, interval, today) do
    cycle_end = calculate_cycle_end(cycle_start, interval)

    # Cycle must have ended (cycle_end < today)
    case Date.compare(today, cycle_end) do
      :gt ->
        # Check if this is the most recent completed cycle
        # by verifying that the next cycle hasn't ended yet (today <= next_end)
        next_start = next_cycle_start(cycle_start, interval)
        next_end = calculate_cycle_end(next_start, interval)

        Date.compare(today, next_end) in [:lt, :eq]

      _ ->
        false
    end
  end

  @spec last_completed_cycle?(Date.t(), interval()) :: boolean()
  def last_completed_cycle?(cycle_start, interval) do
    last_completed_cycle?(cycle_start, interval, Date.utc_today())
  end

  # Private helper functions

  defp monthly_cycle_start(date) do
    Date.new!(date.year, date.month, 1)
  end

  defp monthly_cycle_end(cycle_start) do
    Date.end_of_month(cycle_start)
  end

  defp quarterly_cycle_start(date) do
    quarter_start_month =
      case date.month do
        m when m in [1, 2, 3] -> 1
        m when m in [4, 5, 6] -> 4
        m when m in [7, 8, 9] -> 7
        m when m in [10, 11, 12] -> 10
      end

    Date.new!(date.year, quarter_start_month, 1)
  end

  defp quarterly_cycle_end(cycle_start) do
    # Ensure cycle_start is aligned to quarter boundary
    # This handles cases where cycle_start might not be at the correct quarter start (e.g., month 12)
    aligned_start = quarterly_cycle_start(cycle_start)

    case aligned_start.month do
      1 -> Date.new!(aligned_start.year, 3, 31)
      4 -> Date.new!(aligned_start.year, 6, 30)
      7 -> Date.new!(aligned_start.year, 9, 30)
      10 -> Date.new!(aligned_start.year, 12, 31)
    end
  end

  defp half_yearly_cycle_start(date) do
    half_start_month = if date.month in 1..6, do: 1, else: 7
    Date.new!(date.year, half_start_month, 1)
  end

  defp half_yearly_cycle_end(cycle_start) do
    # Ensure cycle_start is aligned to half-year boundary
    # This handles cases where cycle_start might not be at the correct half-year start (e.g., month 10)
    aligned_start = half_yearly_cycle_start(cycle_start)

    case aligned_start.month do
      1 -> Date.new!(aligned_start.year, 6, 30)
      7 -> Date.new!(aligned_start.year, 12, 31)
    end
  end

  defp yearly_cycle_start(date) do
    Date.new!(date.year, 1, 1)
  end

  defp yearly_cycle_end(cycle_start) do
    Date.new!(cycle_start.year, 12, 31)
  end
end
