# 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 NotifyWith::NotificationAttachedObject

  belongs_to :statistic_profile

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

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

  has_many :tickets
  accepts_nested_attributes_for :tickets, allow_destroy: false

  has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
  has_one :payment_schedule, as: :scheduled, dependent: :destroy

  validates_presence_of :reservable_id, :reservable_type
  validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
  validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
  validates_with ReservationSlotSubscriptionValidator

  attr_accessor :plan_id, :subscription

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

  ##
  # These checks will run before the invoice/payment-schedule is generated
  ##
  def pre_check
    # check that none of the reserved availabilities was locked
    slots.each do |slot|
      raise LockedError if slot.availability.lock
    end
  end

  ## Generate the subscription associated with for the current reservation
  def generate_subscription
    return unless plan_id

    self.subscription = Subscription.new(plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil)
    subscription.init_save
    subscription
  end

  ##
  # These actions will be realized after the reservation is initially saved (on creation)
  ##
  def post_save
    UsersCredits::Manager.new(reservation: self).update_credits
  end

  # @param canceled    if true, count the number of seats for this reservation, including canceled seats
  def total_booked_seats(canceled: false)
    # cases:
    # - machine/training/space reservation => 1 slot = 1 seat (currently not covered by this function)
    # - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
    return 0 if slots.first.canceled_at && !canceled

    total = nb_reserve_places
    total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?

    total
  end

  def user
    statistic_profile.user
  end

  def update_event_nb_free_places
    return unless reservable_type == 'Event'

    reservable.update_nb_free_places
    reservable.save!
  end

  private

  def machine_not_already_reserved
    already_reserved = false
    slots.each do |slot|
      same_hour_slots = Slot.joins(:reservations).where(
        reservations: { reservable_type: reservable_type, reservable_id: reservable_id },
        start_at: slot.start_at,
        end_at: slot.end_at,
        availability_id: slot.availability_id,
        canceled_at: nil
      )
      if same_hour_slots.any?
        already_reserved = true
        break
      end
    end
    errors.add(:machine, 'already reserved') if already_reserved
  end

  def training_not_fully_reserved
    slot = slots.first
    errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed?
  end

  def notify_member_create_reservation
    NotificationCenter.call type: 'notify_member_create_reservation',
                            receiver: user,
                            attached_object: self
  end

  def notify_admin_member_create_reservation
    NotificationCenter.call type: 'notify_admin_member_create_reservation',
                            receiver: User.admins_and_managers,
                            attached_object: self
  end
end
