# frozen_string_literal: true

# Reservation is a Slot or a Ticket booked by a member.
# Slots are for Machine, Space and Training reservations.
# Tickets are for Event reservations.
class Reservation < ApplicationRecord
  include NotificationAttachedObject
  include ICalendarConcern

  belongs_to :statistic_profile

  has_many :slots_reservations, dependent: :destroy
  has_many :slots, through: :slots_reservations

  accepts_nested_attributes_for :slots_reservations, allow_destroy: true
  belongs_to :reservable, polymorphic: true

  has_many :tickets, dependent: :destroy
  accepts_nested_attributes_for :tickets, allow_destroy: false

  has_many :invoice_items, as: :object, dependent: :destroy
  has_one :payment_schedule_object, as: :object, dependent: :destroy

  has_many :prepaid_pack_reservations, dependent: :destroy

  belongs_to :reservation_context
  has_many :booking_users, dependent: :destroy
  accepts_nested_attributes_for :booking_users, allow_destroy: true

  validates :reservable_id, :reservable_type, presence: true
  validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
  validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
  validate :slots_not_locked

  after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
  after_commit :notify_member_create_reservation, on: :create
  after_commit :notify_admin_member_create_reservation, on: :create
  after_commit :extend_subscription, on: :create
  after_commit :notify_member_limitation_reached, on: :create

  delegate :user, to: :statistic_profile

  # @param canceled    if true, count the number of seats for this reservation, including canceled seats
  def total_booked_seats(canceled: false)
    # cases:
    if reservable_type == 'Event'
      # - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
      total = nb_reserve_places
      total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?

      total = 0 unless slots_reservations.first&.canceled_at.nil?
    else
      # - machine/training/space reservation => 1 slot_reservation = 1 seat
      total = slots_reservations.count
      total -= slots_reservations.where.not(canceled_at: nil).count unless canceled
    end

    total
  end

  def update_event_nb_free_places
    return unless reservable_type == 'Event'

    reservable.update_nb_free_places
    reservable.save!
  end

  def original_payment_schedule
    payment_schedule_object&.payment_schedule
  end

  def original_invoice
    invoice_items.select(:invoice_id)
                 .group(:invoice_id)
                 .map(&:invoice_id)
                 .map { |id| Invoice.find_by(id: id, type: nil) }
                 .first
  end

  # Group all slots related to this reservation by dates and by continuous time ranges
  def grouped_slots
    slots_by_date = slots.group_by { |slot| slot[:start_at].to_date }.transform_values { |slots| slots.sort_by { |slot| slot[:start_at] } }
    result = {}
    slots_by_date.each do |date, daily_slots|
      result[date] = { daily_slots.first[:start_at] => [daily_slots.first] }

      daily_slots[1..].each do |slot|
        found = false
        result[date].each do |group_start, group_slots|
          next unless slot[:start_at] == group_slots.last[:end_at]

          result[date][group_start].push(slot)
          found = true
          break
        end
        result[date][slot[:start_at]] = [slot] unless found
      end
    end
    result
  end

  private

  def machine_not_already_reserved
    slots_reservations.each do |slot|
      same_hour_slots = SlotsReservation.joins(:reservation).where(
        reservations: { reservable_type: reservable_type, reservable_id: reservable_id },
        slot_id: slot.slot_id,
        canceled_at: nil
      ).count
      if same_hour_slots.positive?
        errors.add(:reservable, 'already reserved')
        break
      end
    end
  end

  def training_not_fully_reserved
    full = slots_reservations.map(&:slot).map(&:full?).reduce(:&)
    errors.add(:reservable, 'already fully reserved') if full
  end

  def slots_not_locked
    # check that none of the reserved availabilities was locked
    slots.each do |slot|
      errors.add(:slots, 'locked') if slot.availability.lock
    end
  end

  def extend_subscription
    ::Subscriptions::ExtensionAfterReservation.new(self).extend_subscription_if_eligible
  end

  def notify_member_create_reservation
    if reservable_type == 'Event' && reservable.pre_registration?
      NotificationCenter.call type: 'notify_member_pre_booked_reservation',
                              receiver: user,
                              attached_object: self
    else
      NotificationCenter.call type: 'notify_member_create_reservation',
                              receiver: user,
                              attached_object: self
    end
  end

  def notify_admin_member_create_reservation
    if reservable_type == 'Event' && reservable.pre_registration?
      NotificationCenter.call type: 'notify_admin_member_pre_booked_reservation',
                              receiver: User.admins_and_managers,
                              attached_object: self
    else
      NotificationCenter.call type: 'notify_admin_member_create_reservation',
                              receiver: User.admins_and_managers,
                              attached_object: self
    end
  end

  def notify_member_limitation_reached
    date = ReservationLimitService.reached_limit_date(self)
    return if date.nil?

    NotificationCenter.call type: 'notify_member_reservation_limit_reached',
                            receiver: user,
                            attached_object: ReservationLimitService.limit(user.subscribed_plan, reservable),
                            meta_data: { date: date }
  end
end
