# frozen_string_literal: true

# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later

module InsertTickets
  # Will generate rows for payment, offsite_payment or charge, tickets, activities
  # pass in:
  # data: {
  #   tickets: [{quantity, ticket_level_id}],
  #   event_id,
  #   nonprofit_id,
  #   supporter_id,
  #   event_discount_id,
  #   card_id, (if a charge)
  #   offsite_payment: {kind, check_number},
  #   kind (offsite, charge, or free)
  # }
  def self.create(data)
    data = data.with_indifferent_access
    ParamValidation.new(data,
                        tickets: { required: true, is_array: true },
                        nonprofit_id: { required: true, is_reference: true },
                        supporter_id: { required: true, is_reference: true },
                        event_id: { required: true, is_reference: true },
                        event_discount_id: { is_reference: true },
                        kind: { included_in: %w[free charge offsite] },
                        token: { format: UUID::Regex },
                        offsite_payment: { is_hash: true })

    data[:tickets].each do |t|
      ParamValidation.new(t, quantity: { is_integer: true, required: true, min: 1 }, ticket_level_id: { is_reference: true, required: true })
    end

    ParamValidation.new(data[:offsite_payment], kind: { included_in: %w[cash check] }) if data[:offsite_payment] && !data[:offsite_payment][:kind].blank?

    entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id, Event => :event_id)

    entities.merge!(RetrieveActiveRecordItems.retrieve_from_keys(data, { EventDiscount => :event_discount_id }, true))

    tl_entities = get_ticket_level_entities(data)

    validate_entities(entities, tl_entities)

    # verify that enough tickets_available
    QueryTicketLevels.verify_tickets_available(data[:tickets])

    gross_amount = QueryTicketLevels.gross_amount_from_tickets(data[:tickets], data[:event_discount_id])

    result = {}

    if gross_amount > 0
      # Create offsite payment for tickets
      if data[:kind] == 'offsite'
        current_user = data[:current_user]
        # offsite can only come from valid nonprofit users
        unless current_user && QueryRoles.is_authorized_for_nonprofit?(current_user.id, entities[:nonprofit_id].id)
          raise AuthenticationError
        end

        # create payment and offsite payment
        result['payment'] = create_payment(entities, gross_amount)
        result['offsite_payment'] = create_offsite_payment(entities, gross_amount, data, result['payment'])

      # Create charge for tickets
      elsif data['kind'] == 'charge' || !data['kind']
        source_token = QuerySourceToken.get_and_increment_source_token(data[:token], nil)
        QuerySourceToken.validate_source_token_type(source_token)
        tokenizable = source_token.tokenizable
        ## does the card belong to the supporter?
        if tokenizable.holder != entities[:supporter_id]
          raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not own card #{tokenizable.id}", key: :token)
        end

        result = result.merge(InsertCharge.with_stripe(
                                kind: 'Ticket',
                                towards: entities[:event_id].name,
                                metadata: { kind: 'Ticket', event_id: entities[:event_id].id, nonprofit_id: entities[:nonprofit_id].id },
                                statement: "Tickets #{entities[:event_id].name}",
                                amount: gross_amount,
                                nonprofit_id: entities[:nonprofit_id].id,
                                supporter_id: entities[:supporter_id].id,
                                card_id: tokenizable.id
                              ))
        if result['charge']['status'] == 'failed'
          raise ChargeError, result['charge']['failure_message']
        end
      else
        raise ParamValidation::ValidationError.new("Ticket costs money but you didn't pay.", key: :kind)
      end
    end

    # Generate the bid ids
    data['tickets'] = generate_bid_ids(entities[:event_id].id, tl_entities)

    result['tickets'] = generated_ticket_entities(data['tickets'], result, entities)

    # Create the activity rows for the tickets
    InsertActivities.for_tickets(result['tickets'].map(&:id))

    ticket_ids = result['tickets'].map(&:id)
    charge_id =  result['charge'] ? result['charge'].id : nil

    EmailJobQueue.queue(JobTypes::TicketMailerReceiptAdminJob, ticket_ids)
    EmailJobQueue.queue(JobTypes::TicketMailerFollowupJob, ticket_ids, charge_id)
    result
  end

  # Generate a set of 'bid ids' (ids for each ticket scoped within the event)
  def self.generate_bid_ids(event_id, tickets)
    # Generate the bid ids
    last_bid_id = Psql.execute(
      Qexpr.new.select('COUNT(*)').from(:tickets)
      .where('event_id=$id', id: event_id)
    ).first['count'].to_i
    tickets.zip(last_bid_id + 1..last_bid_id + tickets.count).map { |h, id| h.merge('bid_id' => id) }
  end

  # not really needed but used for breaking into the unit test and getting the IDs
  def self.generated_ticket_entities(ticket_data, result, entities)
    ticket_data.map do |ticket_request|
      t = Ticket.new
      t.quantity = ticket_request['quantity']
      t.ticket_level = ticket_request['ticket_level_id']
      t.event = entities[:event_id]
      t.supporter = entities[:supporter_id]
      t.payment = result['payment']
      t.charge = result['charge']
      t.bid_id = ticket_request['bid_id']
      t.event_discount = entities[:event_discount_id]
      t.save!
      t
    end.to_a
  end

  def self.validate_entities(entities, tl_entities)
    ## is supporter deleted? If supporter is deleted, we error!
    if entities[:supporter_id].deleted
      raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} is deleted", key: :supporter_id)
    end

    if entities[:event_id].deleted
      raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} is deleted", key: :event_id)
    end

    # verify that enough tickets_available
    tl_entities.each do |i|
      if i[:ticket_level_id].deleted
        raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} is deleted", key: :tickets)
      end

      if i[:ticket_level_id].event != entities[:event_id]
        raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} does not belong to event #{entities[:event_id]}", key: :tickets)
      end
    end

    # Does the supporter belong to the nonprofit?
    if entities[:supporter_id].nonprofit != entities[:nonprofit_id]
      raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not belong to nonprofit #{entities[:nonprofit_id].id}", key: :supporter_id)
    end

    ## does event belong to nonprofit
    if entities[:event_id].nonprofit != entities[:nonprofit_id]
      raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} does not belong to nonprofit #{entities[:nonprofit_id]}", key: :event_id)
    end

    if entities[:event_discount_id] && entities[:event_discount_id].event != entities[:event_id]
      raise ParamValidation::ValidationError.new("Event discount #{entities[:event_discount_id].id} does not belong to event #{entities[:event_id].id}", key: :event_discount_id)
    end
  end

  def self.get_ticket_level_entities(data)
    data[:tickets].map do |i|
      {
        quantity: i[:quantity],
        ticket_level_id: RetrieveActiveRecordItems.retrieve_from_keys(i, TicketLevel => :ticket_level_id)[:ticket_level_id]
      }
    end.to_a
  end

  def self.create_payment(entities, gross_amount)
    p = Payment.new
    p.gross_amount = gross_amount
    p.nonprofit = entities[:nonprofit_id]
    p.supporter = entities[:supporter_id]
    p.refund_total = 0
    p.date = Time.current
    p.towards = entities[:event_id].name
    p.fee_total = 0
    p.net_amount = gross_amount
    p.kind = 'OffsitePayment'
    p.save!
    p
  end

  def self.create_offsite_payment(entities, gross_amount, data, payment)
    p = OffsitePayment.new
    p.gross_amount = gross_amount
    p.nonprofit = entities[:nonprofit_id]
    p.supporter = entities[:supporter_id]
    p.date = Time.current
    p.payment = payment
    p.kind = data['offsite_payment']['kind']
    p.check_number = data['offsite_payment']['check_number']
    p.save!
    p
  end
end