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

# wrapping your API routes in an ApiRouteSet adds structure to the routes file
# and lets us auto-discover the route for a given API method in the docs.
class ApiRouteSet
  attr_reader :prefix

  def initialize(prefix)
    @prefix = prefix
  end
  attr_accessor :mapper

  def self.draw(router, prefix = self.prefix, &)
    @@prefixes ||= Set.new
    @@prefixes << prefix
    route_set = new(prefix)
    route_set.mapper = router
    route_set.instance_eval(&)
  ensure
    route_set.mapper = nil
  end

  def self.prefixes
    @@prefixes
  end

  def self.prefix
    raise ArgumentError, "prefix required"
  end

  def self.routes_for(prefix)
    CanvasRails::Application.routes.set.select { |r| r.path.spec.to_s.start_with?(prefix) }
  end

  def self.segments_match(seg1, seg2)
    seg1.size == seg2.size && seg1.each_with_index { |s, i| return false unless s.respond_to?(:value) && s.value == seg2[i].value }
  end

  def self.api_methods_for_controller_and_action(controller, action)
    @routes ||= prefixes.map { |pfx| routes_for(pfx) }.flatten
    @routes.find_all { |r| matches_controller_and_action?(r, controller, action) }
  end

  def self.matches_controller_and_action?(route, controller, action)
    route.requirements[:controller] == controller && route.requirements[:action] == action
  end

  def method_missing(m, *a, &b)
    mapper.__send__(m, *a) do
      instance_eval(&b) if b
    end
  end

  def get(path, opts = {})
    route(:get, path, opts)
  end

  def put(path, opts = {})
    route(:put, path, opts)
  end

  def post(path, opts = {})
    route(:post, path, opts)
  end

  def delete(path, opts = {})
    route(:delete, path, opts)
  end

  def patch(path, opts = {})
    route(:patch, path, opts)
  end

  def resources(resource_name, opts = {})
    resource_name = resource_name.to_s

    path = opts.delete(:path) || resource_name
    name_prefix = opts.delete :name_prefix

    only, except = opts.delete(:only), opts.delete(:except)
    maybe_action = ->(action) { (!only || Array(only).include?(action)) && (!except || !Array(except).include?(action)) }

    get(path.to_s, opts.merge(action: :index, as: "#{name_prefix}#{resource_name}")) if maybe_action[:index]
    get("#{path}/:#{resource_name.singularize}_id", opts.merge(action: :show, as: "#{name_prefix}#{resource_name.singularize}")) if maybe_action[:show]
    post(path.to_s, opts.merge(action: :create, as: (maybe_action[:index] ? nil : "#{name_prefix}#{resource_name}"))) if maybe_action[:create]
    put("#{path}/:#{resource_name.singularize}_id", opts.merge(action: :update)) if maybe_action[:update]
    delete("#{path}/:#{resource_name.singularize}_id", opts.merge(action: :destroy)) if maybe_action[:destroy]
  end

  def mapper_prefix
    ""
  end

  def route(method, path, opts)
    opts ||= {}
    opts[:as] ||= opts.delete(:path_name)
    opts[:as] = "#{mapper_prefix}#{opts[:as]}" if opts[:as]
    opts[:constraints] ||= {}
    opts[:constraints][:format] = "json" if opts[:constraints].is_a? Hash
    opts[:format] = "json"
    mapper.send(method, "#{prefix}/#{path}", opts)
  end

  class V1 < ::ApiRouteSet
    # match a path component, including periods, but excluding the string ".json" for backwards compat
    # for api v2, we'll just drop the .json completely
    # unfortunately, this means that api v1 can't match a sis id that ends with
    # .json -- but see the api docs for info on sending hex-encoded sis ids,
    # which allows any string.
    ID_REGEX = %r{(?:[^/?.]|\.(?!json(?:\z|[/?])))+}
    ID_PARAM = /^:(id|\w+_id)$/

    def self.prefix
      "/api/v1"
    end

    def mapper_prefix
      "api_v1_"
    end

    def route(method, path, opts)
      opts[:constraints] ||= {}
      path.split("/").each { |segment| opts[:constraints][segment[1..].to_sym] = ID_REGEX if segment.match(ID_PARAM) }
      super
    end
  end

  # Hack around rails routing deficiency: setting constraints on a path segment
  # not only affects parsing, but also causes constructing a url to fail if the
  # segment doesn't match the constraint. (see git blame, INTEROP-6659, and
  # https://github.com/rails/rails/issues/43466)
  #
  # NOTE: using rails helpers with IDs ending in ".json" will result in
  # incorrect/invalid URLs; a fix would require even more monkey-patching.
  module ConstraintsBugHackRequirements
    def requirements
      @_constraints_bug_hack_requirements ||= super.reject { |_k, v| v.equal? ApiRouteSet::V1::ID_REGEX } # rubocop:disable Naming/MemoizedInstanceVariableName
    end
  end

  ActionDispatch::Journey::Path::Pattern.prepend ConstraintsBugHackRequirements
end
