# frozen_string_literal: true

#
# Copyright (C) 2014 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require "ims/lti"

module Lti
  module IMS
    class ToolSettingController < ApplicationController
      include Lti::ApiServiceHelper
      include Lti::IMS::AccessTokenHelper

      rescue_from ActiveRecord::RecordNotFound do
        render json: {
                 status: I18n.t("lib.auth.api.not_found_status", "not_found"),
                 errors: [{ message: I18n.t("lib.auth.api.not_found_message", "not_found") }]
               },
               status: :not_found
      end

      TOOL_SETTINGS_SERVICE = "ToolProxySettings"
      TOOL_PROXY_BINDING_SERVICE = "ToolProxyBindingSettings"
      LTI_LINK_SETTINGS = "LtiLinkSettings"

      skip_before_action :load_user
      before_action :authenticate_api_call
      skip_before_action :verify_authenticity_token

      SERVICE_DEFINITIONS = [
        {
          id: TOOL_SETTINGS_SERVICE,
          endpoint: "api/lti/tool_settings/tool_proxy/{tool_proxy_id}",
          format: %w[
            application/vnd.ims.lti.v2.toolsettings+json
            application/vnd.ims.lti.v2.toolsettings.simple+json
          ].freeze,
          action: %w[GET PUT].freeze
        }.freeze,
        {
          id: TOOL_PROXY_BINDING_SERVICE,
          endpoint: "api/lti/tool_settings/bindings/{binding_id}",
          format: %w[
            application/vnd.ims.lti.v2.toolsettings+json'
            application/vnd.ims.lti.v2.toolsettings.simple+json
          ].freeze,
          action: %w[GET PUT].freeze
        }.freeze,
        {
          id: LTI_LINK_SETTINGS,
          endpoint: "api/lti/tool_proxy/{tool_proxy_guid}/courses/{course_id}/resource_link_id/{resource_link_id}/tool_setting",
          format: %w[
            application/vnd.ims.lti.v2.toolsettings+json
            application/vnd.ims.lti.v2.toolsettings.simple+json
          ].freeze,
          action: %w[GET PUT].freeze
        }.freeze
      ].freeze

      def show
        render_bad_request and return unless valid_show_request?

        render json: tool_setting_json(tool_setting, params[:bubble]), content_type: @content_type
      end

      def update
        json = JSON.parse(request.body.read)
        render_bad_request and return unless valid_update_request?(json)

        tool_setting.update_attribute(:custom, custom_settings(tool_setting_type(tool_setting), json))
        head :ok
      end

      def lti2_service_name
        [TOOL_SETTINGS_SERVICE, TOOL_PROXY_BINDING_SERVICE, LTI_LINK_SETTINGS]
      end

      private

      def tool_setting_json(tool_setting, bubble)
        if %w[all distinct].include?(bubble)
          graph = []
          distinct = (bubble == "distinct") ? [] : nil
          while tool_setting
            graph << collect_tool_settings(tool_setting, distinct)
            distinct |= graph.last.custom.keys if distinct
            case tool_setting_type(tool_setting)
            when "LtiLink"
              tool_setting = ToolSetting.where(tool_proxy_id: tool_setting.tool_proxy_id, context_type: tool_setting.context_type, context_id: tool_setting.context_id, resource_link_id: nil).first
            when "ToolProxyBinding"
              tool_setting = ToolSetting.where(tool_proxy_id: tool_setting.tool_proxy_id, context_type: nil, context_id: nil, resource_link_id: nil).first
            when "ToolProxy"
              tool_setting = nil
            end
          end

          if request.headers["accept"].include?("application/vnd.ims.lti.v2.toolsettings+json")
            @content_type = "application/vnd.ims.lti.v2.toolsettings+json"
            ::IMS::LTI::Models::ToolSettingContainer.new(graph:)
          elsif bubble == "distinct" && request.headers["accept"].include?("application/vnd.ims.lti.v2.toolsettings.simple+json")
            @content_type = "application/vnd.ims.lti.v2.toolsettings.simple+json"
            custom = {}
            graph.reverse_each { |ts| custom.merge!(ts.custom) }
            custom
          end
        elsif request.headers["accept"].include?("application/vnd.ims.lti.v2.toolsettings+json")
          @content_type = "application/vnd.ims.lti.v2.toolsettings+json"
          ::IMS::LTI::Models::ToolSettingContainer.new(graph: [collect_tool_settings(tool_setting)])
        else
          @content_type = "application/vnd.ims.lti.v2.toolsettings.simple+json"
          tool_setting.custom || {}
        end
      end

      def collect_tool_settings(tool_setting, distinct = nil)
        type = tool_setting_type(tool_setting)
        url = show_lti_tool_settings_url(tool_setting.id)
        custom = tool_setting.custom || {}
        custom.delete_if { |k, _| distinct.include? k } if distinct
        ::IMS::LTI::Models::ToolSetting.new(custom:, type:, id: url)
      end

      def custom_settings(type, json)
        if request.media_type == "application/vnd.ims.lti.v2.toolsettings+json"
          setting = json["@graph"].find { |s| s["@type"] == type }
          setting["custom"]
        else
          json
        end
      end

      def tool_setting_type(tool_setting)
        if tool_setting.resource_link_id.present?
          "LtiLink"
        elsif tool_setting.context.present?
          "ToolProxyBinding"
        else
          "ToolProxy"
        end
      end

      def authenticate_api_call
        if oauth2_request?
          begin
            validate_access_token!
          rescue Lti::OAuth2::InvalidTokenError
            render_unauthorized and return
          end
        elsif request.authorization.present?
          lti_authenticate or return
        else
          render_unauthorized and return
        end
      end

      def tool_setting
        @_tool_setting ||= begin
          tool_setting_id = params[:tool_setting_id]
          ts = if tool_setting_id.present?
                 tool_proxy.tool_settings.find(tool_setting_id)
               else
                 get_context
                 tool_proxy_guid = params[:tool_proxy_guid]
                 resource_link_id = params[:resource_link_id]
                 render_unauthorized and return unless tool_proxy_guid == tool_proxy.guid # rubocop:disable Lint/NoReturnInBeginEndBlocks

                 tool_proxy.tool_settings.find_by(
                   context: @context,
                   resource_link_id:
                 )
               end
          raise ActiveRecord::RecordNotFound if ts.blank?

          ts
        end
      end

      def valid_show_request?
        # TODO: register a mime type in rails for these content-types
        params[:bubble].blank? ||
          params[:bubble] == "distinct" ||
          (params[:bubble] == "all" && request.accept.include?("application/vnd.ims.lti.v2.toolsettings+json"))
      end

      def valid_update_request?(json)
        valid = params[:bubble].blank?
        if valid && request.media_type == "application/vnd.ims.lti.v2.toolsettings+json"
          valid = json["@graph"].count == 1
        elsif valid && request.media_type == "application/vnd.ims.lti.v2.toolsettings.simple+json"
          valid = !json.key?("@graph")
        end
        valid
      end

      def render_bad_request
        render json: {
                 status: I18n.t("lib.auth.api.bad_request_status", "bad_request"),
                 errors: [{ message: I18n.t("lib.auth.api.bad_request_message", "bad_request") }]
               },
               status: :bad_request
      end
    end
  end
end
