# 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/>.
#

module Api
  module Html
    # maps canvas URLs to API URL helpers
    # target array is return type, helper, name of each capture, and optionally a Hash of extra arguments
    API_ROUTE_MAP = {
      # list discussion topics
      %r{^/courses/(#{ID})/discussion_topics$} => ["[Discussion]", :api_v1_course_discussion_topics_url, :course_id],
      %r{^/groups/(#{ID})/discussion_topics$} => ["[Discussion]", :api_v1_group_discussion_topics_url, :group_id],

      # get a single topic
      %r{^/courses/(#{ID})/discussion_topics/(#{ID})$} => ["Discussion", :api_v1_course_discussion_topic_url, :course_id, :topic_id],
      %r{^/groups/(#{ID})/discussion_topics/(#{ID})$} => ["Discussion", :api_v1_group_discussion_topic_url, :group_id, :topic_id],

      # List pages
      %r{^/courses/(#{ID})/wiki$} => ["[Page]", :api_v1_course_wiki_pages_url, :course_id],
      %r{^/groups/(#{ID})/wiki$} => ["[Page]", :api_v1_group_wiki_pages_url, :group_id],
      %r{^/courses/(#{ID})/pages$} => ["[Page]", :api_v1_course_wiki_pages_url, :course_id],
      %r{^/groups/(#{ID})/pages$} => ["[Page]", :api_v1_group_wiki_pages_url, :group_id],

      # Show page
      %r{^/courses/(#{ID})/wiki/([^/?]+)(?:\?[^/]+)?} => ["Page", :api_v1_course_wiki_page_url, :course_id, :url_or_id],
      %r{^/groups/(#{ID})/wiki/([^/?]+)(?:\?[^/]+)?} => ["Page", :api_v1_group_wiki_page_url, :group_id, :url_or_id],
      %r{^/courses/(#{ID})/pages/([^/?]+)(?:\?[^/]+)?} => ["Page", :api_v1_course_wiki_page_url, :course_id, :url_or_id],
      %r{^/groups/(#{ID})/pages/([^/?]+)(?:\?[^/]+)?} => ["Page", :api_v1_group_wiki_page_url, :group_id, :url_or_id],

      # List assignments
      %r{^/courses/(#{ID})/assignments$} => ["[Assignment]", :api_v1_course_assignments_url, :course_id],

      # Get assignment
      %r{^/courses/(#{ID})/assignments/(#{ID})$} => ["Assignment", :api_v1_course_assignment_url, :course_id, :id],

      # List files
      %r{^/courses/(#{ID})/files$} => ["Folder", :api_v1_course_folder_url, :course_id, { id: "root" }],
      %r{^/groups/(#{ID})/files$} => ["Folder", :api_v1_group_folder_url, :group_id, { id: "root" }],
      %r{^/users/(#{ID})/files$} => ["Folder", :api_v1_user_folder_url, :user_id, { id: "root" }],

      # Get file
      %r{^/courses/(#{ID})/files/(#{ID})} => ["File", :api_v1_course_attachment_url, :course_id, :id],
      %r{^/groups/(#{ID})/files/(#{ID})} => ["File", :api_v1_group_attachment_url, :group_id, :id],
      %r{^/users/(#{ID})/files/(#{ID})} => ["File", :api_v1_user_attachment_url, :user_id, :id],
      %r{^/files/(#{ID})} => ["File", :api_v1_attachment_url, :id],

      # List quizzes
      %r{^/courses/(#{ID})/quizzes$} => ["[Quiz]", :api_v1_course_quizzes_url, :course_id],

      # Get quiz
      %r{^/courses/(#{ID})/quizzes/(#{ID})$} => ["Quiz", :api_v1_course_quiz_url, :course_id, :id],

      # List modules
      %r{^/courses/(#{ID})/modules$} => ["[Module]", :api_v1_course_context_modules_url, :course_id],

      # Get module
      %r{^/courses/(#{ID})/modules/(#{ID})$} => ["Module", :api_v1_course_context_module_url, :course_id, :id],

      # Launch LTI tool
      %r{^/courses/(#{ID})/external_tools/retrieve\?url=(.*)$} => ["SessionlessLaunchUrl", :api_v1_course_external_tool_sessionless_launch_url, :course_id, :url],
    }.freeze

    class UrlProxy
      attr_reader :proxy, :context, :host, :protocol, :target_shard

      def initialize(helper, context, host, protocol, target_shard: nil)
        @proxy = helper
        @context = context
        @host = host
        @protocol = protocol
        @target_shard = target_shard || context.shard
      end

      def media_object_thumbnail_url(media_id)
        proxy.media_object_thumbnail_url(media_id, width: 550, height: 448, type: 3, host:, protocol:)
      end

      def media_context
        # not all objects are clean to find media urls for.
        # If the current context is one of those classes,
        # then we should look up what the actual context is that
        # would have a reasonable media url on it (usually a course).
        # If you are trying to do "media_redirect_url" on a context
        # where you're finding a media url helper doesn't exist, you probably
        # need to add a case here.
        case context
        when Announcement
          context.context
        when CourseSection, DiscussionTopic, Group, WikiPage
          context.course
        when DiscussionEntry
          context.discussion_topic.course
        when Message
          context.media_context
        else
          context
        end
      end

      def media_redirect_url(media_id, media_type)
        proxy.polymorphic_url([media_context, :media_download], entryId: media_id, media_type:, redirect: "1", host:, protocol:)
      end

      def show_media_tracks_url(media_object_id, media_id)
        proxy.show_media_tracks_url(media_object_id, media_id, format: :json, host:, protocol:)
      end

      # rewrite any html attributes that are urls but just absolute paths, to
      # have the canvas domain prepended to make them a full url
      #
      # relative urls and invalid urls are currently ignored
      def rewrite_api_urls(element, attributes)
        attributes.each do |attribute|
          url_str = element[attribute]
          begin
            url = URI.parse(url_str)
            # if the url_str is "//example.com/a", the parsed url will have a host set
            # otherwise if it starts with a slash, it's a path that needs to be
            # made absolute with the canvas hostname prepended
            if !url.host && url_str[0] == "/"[0]
              # transpose IDs in the URL
              if context.shard != target_shard && (args = recognize_path(url_str)) && args[:controller] != absolute_controller("equation_images")
                transpose_ids(args)
                args[:host] = host
                args[:protocol] = protocol
                element[attribute] = proxy.url_for(args)
              else
                element[attribute] = "#{protocol}://#{host}#{url_str}"
              end
              api_endpoint_info(url_str).each do |att, val|
                element[att] = val
              end
            end
          rescue URI::Error
            # leave it as is
          end
        end
      end

      def api_endpoint_info(url)
        API_ROUTE_MAP.each_pair do |re, api_route|
          match = re.match(url)
          next unless match

          return_type = api_route[0]
          helper = api_route[1]
          args = { protocol:, host: }
          args.merge! (api_route.slice(2, match.captures.size).zip match.captures).to_h
          # transpose IDs in the URL
          transpose_ids(args) if context.shard != target_shard
          if args[:url] && (return_type == "SessionlessLaunchUrl" || (return_type == "Page" && url.include?("titleize=0")))
            args[:url] = URI::DEFAULT_PARSER.unescape(args[:url])
          end
          api_route.slice(match.captures.size + 2, 1).each { |opts| args.merge!(opts) }
          return { "data-api-endpoint" => proxy.send(helper, args), "data-api-returntype" => return_type }
        end
        {}
      end

      # based on ActionDispatch::Routing::RouteSet#recognize_path, but returning all params,
      # not just path_params. and failures return nil, not raise an exception
      def recognize_path(path)
        path = ActionDispatch::Journey::Router::Utils.normalize_path(path)

        begin
          env = Rack::MockRequest.env_for(path, method: "GET")
        rescue URI::InvalidURIError
          return nil
        end

        req = Rails.application.routes.send(:make_request, env)
        Rails.application.routes.router.recognize(req) do |route, params|
          params.each do |key, value|
            next unless value.is_a?(String)

            value = value.dup.force_encoding(Encoding::BINARY)
            value = absolute_controller(value) if key == :controller
            params[key] = URI::DEFAULT_PARSER.unescape(value)
          end
          req.path_parameters = params
          app = route.app
          if app.matches?(req) && app.dispatcher?
            return req.params
          end
        end

        nil
      end

      def absolute_controller(controller_string)
        # Use an absolute controller path, otherwise
        # `#url_for` attempts to maintain the namespace of
        # the @proxy class.
        "/#{controller_string}"
      end

      def transpose_ids(args)
        args.each_key do |key|
          if (key.to_s == "id" || key.to_s.end_with?("_id")) &&
             (new_id = Switchman::Shard.relative_id_for(args[key], context.shard, target_shard))
            args[key] = Switchman::Shard.short_id_for(new_id)
          end
        end
      end
    end
  end
end
