# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
#
# Copyright (c) 2016 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see .
class BbbController < ApplicationController
  include BbbApi
  before_action :authorize_recording_owner!, only: [:update_recordings, :delete_recordings]
  before_action :load_and_authorize_room_owner!, only: [:end]
  skip_before_action :verify_authenticity_token, only: :callback
  # GET /:resource/:id/join
  # GET /:resource/:room_id/:id/join
  def join
    if params[:name].blank?
      return render_bbb_response(
        messageKey: "missing_parameter",
        message: "user name was not included",
        status: :unprocessable_entity
      )
    elsif params[:name].size > user_name_limit
      return render_bbb_response(
        messageKey: "invalid_parameter",
        message: "user name is too long",
        status: :unprocessable_entity
      )
    elsif params[:id].size > meeting_name_limit
      return render_bbb_response(
        messageKey: "invalid_parameter",
        message: "meeting name is too long",
        status: :unprocessable_entity
      )
    else
      if params[:room_id]
        user = User.find_by encrypted_id: params[:room_id]
        if !user
          return render_bbb_response(
            messageKey: "not_found",
            message: "User Not Found",
            status: :not_found
          )
        end
        meeting_id = "#{params[:room_id]}-#{params[:id]}"
        meeting_name = params[:id]
        meeting_path = "#{URI.encode(params[:room_id])}/#{URI.encode(params[:id]).gsub('/','%2F')}"
      else
        user = User.find_by encrypted_id: params[:id]
        meeting_id = params[:id]
        meeting_path = URI.encode(meeting_id).gsub('/','%2F')
      end
      options = if user
        {
          wait_for_moderator: true,
          meeting_recorded: true,
          meeting_name: meeting_name,
          room_owner: params[:room_id],
          user_is_moderator: current_user == user
        }
      else
        {
          user_is_moderator: true
        }
      end
      base_url = "#{request.base_url}#{relative_root}/#{params[:resource]}/#{meeting_path}"
      options[:meeting_logout_url] = base_url
      options[:hook_url] = "#{base_url}/callback"
      options[:moderator_message] = t('moderator_default_message', url: "#{base_url}")
      bbb_res = bbb_join_url(
        meeting_id,
        params[:name],
        options
      )
      # the user can join the meeting
      if user
        if bbb_res[:returncode] && current_user == user
          JoinMeetingJob.perform_later(user.encrypted_id, params[:id])
      # user will be waiting for a moderator
        else
          NotifyUserWaitingJob.perform_later(user.encrypted_id, params[:id], params[:name])
        end
      end
      render_bbb_response bbb_res, bbb_res[:response]
    end
  end
  # POST /:resource/:room_id/:id/callback
  # Endpoint for webhook calls from BigBlueButton
  def callback
    # respond with 200 anyway so BigBlueButton knows the hook call was ok
    # but abort execution
    head(:ok) && return unless validate_checksum
    begin
      data = JSON.parse(read_body(request))
      treat_callback_event(data["event"])
    rescue Exception => e
      logger.error "Error parsing webhook data. Data: #{data}, exception: #{e.inspect}"
      # respond with 200 anyway so BigBlueButton knows the hook call was ok
      head(:ok) && return
    end
  end
  # DELETE /rooms/:room_id/:id/end
  def end
    load_and_authorize_room_owner!
    bbb_res = bbb_end_meeting "#{@user.encrypted_id}-#{params[:id]}"
    if bbb_res[:returncode] || bbb_res[:status] == :not_found
      EndMeetingJob.perform_later(@user.encrypted_id, params[:id])
      bbb_res[:status] = :ok
    end
    render_bbb_response bbb_res
  end
  # GET /rooms/:room_id/recordings
  # GET /rooms/:room_id/:id/recordings
  def recordings
    load_room!
    # bbb_res = bbb_get_recordings "#{@user.encrypted_id}-#{params[:id]}"
    options = { "meta_room-id": @user.encrypted_id }
    if params[:id]
      options["meta_meeting-name"] = params[:id]
    end
    bbb_res = bbb_get_recordings(options)
    render_bbb_response bbb_res, bbb_res[:recordings]
  end
  # PATCH /rooms/:room_id/recordings/:record_id
  # PATCH /rooms/:room_id/:id/recordings/:record_id
  def update_recordings
    published = params[:published] == 'true'
    metadata = params.select{ |k, v| k.match(/^meta_/) }
    bbb_res = bbb_update_recordings(params[:record_id], published, metadata)
    if bbb_res[:returncode]
      RecordingUpdatesJob.perform_later(@user.encrypted_id, params[:record_id])
    end
    render_bbb_response bbb_res
  end
  # DELETE /rooms/:room_id/recordings/:record_id
  # DELETE /rooms/:room_id/:id/recordings/:record_id
  def delete_recordings
    recording = bbb_get_recordings({recordID: params[:record_id]})[:recordings].first
    bbb_res = bbb_delete_recordings(params[:record_id])
    if bbb_res[:returncode]
      RecordingDeletesJob.perform_later(@user.encrypted_id, params[:record_id], recording[:metadata][:"meeting-name"])
    end
    render_bbb_response bbb_res
  end
  private
  def load_room!
    @user = User.find_by encrypted_id: params[:room_id]
    if !@user
      render head(:not_found) && return
    end
  end
  def load_and_authorize_room_owner!
    load_room!
    if !current_user || current_user != @user
      render head(:unauthorized) && return
    end
  end
  def authorize_recording_owner!
    load_and_authorize_room_owner!
    recordings = bbb_get_recordings({recordID: params[:record_id]})[:recordings]
    recordings.each do |recording|
      if recording[:recordID] == params[:record_id]
        return true
      end
    end
    render head(:not_found) && return
  end
  def render_bbb_response(bbb_res, response={})
    @messageKey = bbb_res[:messageKey]
    @message = bbb_res[:message]
    @status = bbb_res[:status]
    @response = response
    render status: @status
  end
  def read_body(request)
    request.body.read.force_encoding("UTF-8")
  end
  def treat_callback_event(event)
    eventName = (event.present? && event['header'].present?) ? event['header']['name'] : nil
    # a recording is ready
    if eventName == "publish_ended"
      if event['payload'] && event['payload']['metadata'] && event['payload']['meeting_id']
        token = event['payload']['metadata'][META_TOKEN]
        room_id = event['payload']['metadata']['room-id']
        record_id = event['payload']['meeting_id']
        # the webhook event doesn't have all the data we need, so we need
        # to send a getRecordings anyway
        # TODO: if the webhooks included all data in the event we wouldn't need this
        rec_info = bbb_get_recordings({recordID: record_id})
        rec_info = rec_info[:recordings].first
        RecordingCreatedJob.perform_later(token, room_id, parse_recording_for_view(rec_info))
        # send an email to the owner of this recording, if defined
        if Rails.configuration.mail_notifications
          owner = User.find_by(encrypted_id: room_id)
          RecordingReadyEmailJob.perform_later(owner, parse_recording_for_view(rec_info)) if owner.present?
        end
        # TODO: remove the webhook now that the meeting and recording are done
        # remove only if the meeting is not running, otherwise the hook is needed
        # if Rails.configuration.use_webhooks
        #   webhook_remove("#{base_url}/callback")
        # end
      else
        logger.error "Bad format for event #{event}, won't process"
      end
    else
      logger.info "Callback event will not be treated. Event name: #{eventName}"
    end
    render head(:ok) && return
  end
  # Validates the checksum received in a callback call.
  # If the checksum doesn't match, renders an ok and aborts execution.
  def validate_checksum
    secret = ENV['BIGBLUEBUTTON_SECRET']
    checksum = params["checksum"]
    data = read_body(request)
    callback_url = uri_remove_param(request.original_url, "checksum")
    checksum_str = "#{callback_url}#{data}#{secret}"
    calculated_checksum = Digest::SHA1.hexdigest(checksum_str)
    if calculated_checksum != checksum
      logger.error "Checksum did not match. Calculated: #{calculated_checksum}, received: #{checksum}"
      false
    else
      true
    end
  end
  # Removes parameters from an URI
  def uri_remove_param(uri, params = nil)
    return uri unless params
    params = Array(params)
    uri_parsed = URI.parse(uri)
    return uri unless uri_parsed.query
    new_params = uri_parsed.query.gsub(/&/, '&').split('&').reject { |q| params.include?(q.split('=').first) }
    uri = uri.split('?').first
    if new_params.count > 0
      "#{uri}?#{new_params.join('&')}"
    else
      uri
    end
  end
end