# frozen_string_literal: true

#
# Copyright (C) 2011 - 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 "nokogiri"

module CC::Importer
  class BLTIConverter
    class CCImportError < RuntimeError; end
    include CC::Importer

    def get_blti_resources(manifest)
      blti_resources = []

      manifest.css("resource[type=#{BASIC_LTI}]").each do |r_node|
        res = {}
        res[:migration_id] = r_node["identifier"]
        res[:href] = r_node["href"]
        res[:files] = []
        r_node.css("file").each do |file_node|
          res[:files] << { href: file_node[:href] }
        end

        blti_resources << res
      end

      blti_resources
    end

    def convert_blti_links(blti_resources, converter)
      tools = []

      blti_resources.each do |res|
        path = res[:href] || (res[:files]&.first && res[:files].first[:href])
        path = converter.get_full_path(path)

        next unless File.exist?(path)

        doc = open_file_xml(path)
        tool = convert_blti_link(doc)
        tool[:migration_id] = res[:migration_id]
        res[:url] = tool[:url] # for the organization item to reference

        tools << tool
      end

      tools
    end

    def convert_blti_link(doc)
      blti = get_blti_namespace(doc)
      blti = nil unless doc.namespaces["xmlns:#{blti}"]
      link_css_path = "cartridge_basiclti_link"
      tool = {}
      tool[:description] = get_node_val(doc, "#{link_css_path} > #{blti}|description")&.strip
      tool[:title] = get_node_val(doc, "#{link_css_path} > #{blti}|title")&.strip
      tool[:url] = get_node_val(doc, "#{link_css_path} > #{blti}|secure_launch_url")
      tool[:url] ||= get_node_val(doc, "#{link_css_path} > #{blti}|launch_url")
      tool[:url] &&= tool[:url].strip
      if (custom_node = doc.css("#{link_css_path} > #{blti}|custom").first)
        tool[:custom_fields] = get_custom_properties(custom_node)
      end
      tool[:custom_fields] ||= {}

      doc.css("#{link_css_path} > #{blti}|extensions").each do |extension|
        tool[:extensions] ||= []
        ext = {}
        ext[:platform] = extension["platform"]
        ext[:custom_fields] = get_custom_properties(extension)

        if ext[:platform] == CANVAS_PLATFORM
          tool[:privacy_level] = ext[:custom_fields].delete "privacy_level"
          tool[:not_selectable] = ext[:custom_fields].delete "not_selectable"
          tool[:domain] = ext[:custom_fields].delete "domain"
          tool[:consumer_key] = ext[:custom_fields].delete "consumer_key"
          tool[:shared_secret] = ext[:custom_fields].delete "shared_secret"
          tool[:tool_id] = ext[:custom_fields].delete "tool_id"
          tool[:lti_version] = ext[:custom_fields].delete "lti_version"
          if (tool[:assignment_points_possible] = ext[:custom_fields].delete("outcome"))
            tool[:assignment_points_possible] = tool[:assignment_points_possible].to_f
          end
          tool[:settings] = ext[:custom_fields]
        else
          tool[:extensions] << ext
        end
      end
      if (icon = get_node_val(doc, "#{link_css_path} > #{blti}|icon"))
        tool[:settings] ||= {}
        tool[:settings][:icon_url] = icon.strip
      end
      tool
    end

    def convert_blti_xml(xml)
      doc = create_xml_doc(xml)
      unless doc.namespaces.to_s.downcase.include? "imsglobal"
        raise CCImportError, I18n.t("Invalid XML Configuration")
      end

      begin
        tool = convert_blti_link(doc)
        check_for_unescaped_url_properties(tool) if tool
      rescue Nokogiri::XML::XPath::SyntaxError
        raise CCImportError, I18n.t(:invalid_xml_syntax, "Invalid xml syntax")
      end
      tool
    end

    def check_for_unescaped_url_properties(obj)
      # Recursively look for properties named 'url'
      case obj
      when Hash
        obj.select { |k, v| k.to_s == "url" && v.is_a?(String) }
           .each_value { |v| check_for_unescaped_url(v) }
        obj.each_value { |v| check_for_unescaped_url_properties(v) }
      when Array
        obj.each { |o| check_for_unescaped_url_properties(o) }
      end
    end

    def check_for_unescaped_url(url)
      if /(.*[^=]*\?*=)[^&;]*=/.match?(url)
        raise CCImportError, I18n.t(:invalid_url_in_xml, "Invalid url in xml. Ampersands must be escaped.")
      end
    end

    def retrieve_and_convert_blti_url(url)
      InstrumentTLSCiphers.without_tls_metrics do
        response = CanvasHttp.get(url, redirect_limit: 10)
        config_xml = response.body
        convert_blti_xml(config_xml)
      rescue Timeout::Error
        raise CCImportError, I18n.t(:retrieve_timeout, "could not retrieve configuration, the server response timed out")
      end
    end

    def get_custom_properties(node)
      props = {}
      node.children.each do |property|
        next if property.name == "text"

        case property.name
        when "property"
          props[property["name"]] = property.text.strip
        when "options"
          props[property["name"]] = get_custom_properties(property)
        when "custom"
          props[:custom_fields] = get_custom_properties(property)
        end
      end
      props
    end

    def get_blti_namespace(doc)
      doc.namespaces.each_pair do |key, val|
        if val == BLTI_NAMESPACE
          return key.gsub("xmlns:", "")
        end
      end
      "blti"
    end

    def create_assignments_from_lti_links(lti_tools)
      asmnts = []

      lti_tools.each do |tool|
        next unless tool[:assignment_points_possible]

        asmnt = { migration_id: tool[:migration_id] }
        asmnt[:title] = tool[:title]
        asmnt[:description] = tool[:description]
        asmnt[:submission_format] = "external_tool"
        asmnt[:external_tool_url] = tool[:url]
        asmnt[:grading_type] = "points"
        asmnt[:points_possible] = tool[:assignment_points_possible]
        asmnts << asmnt
      end

      asmnts
    end
  end
end
