# frozen_string_literal: true

module Routing
  module OrganizationsHelper
    extend ActiveSupport::Concern

    # Provides organization-aware url helpers by automatically switching between
    # organization-scoped routes (/o/:organization_path/...) and global routes
    # based on the current organization context.
    #
    # This class iterates through existing URL helpers and maps between global
    # and Organization helpers by matching helper names.
    class MappedHelpers
      ORGANIZATION_PATH_PATTERN = '/o/:organization_path'
      ORGANIZATION_PATH_REGEX = %r{(?<=^|_)organizations?_}
      PATH_SUFFIX = '_path'
      URL_SUFFIX = '_url'

      mattr_accessor :already_installed, default: false

      def self.install
        return if already_installed

        url_helpers = Gitlab::Application.routes.url_helpers

        # Preserve original paths for use in specific circumstances.
        alias_unscoped(url_helpers, :root_url)
        alias_unscoped(url_helpers, :root_path)
        alias_unscoped(url_helpers, :group_canonical_url)
        alias_unscoped(url_helpers, :group_canonical_path)

        # Override URL helpers to be Organization context aware.
        route_pairs = find_route_pairs
        override_module = build_override_module(route_pairs)
        url_helpers.prepend(override_module)

        self.already_installed = true
      end

      def self.alias_unscoped(url_helpers, existing_method)
        unscoped_method = "unscoped_#{existing_method}"
        return unless url_helpers.respond_to?(existing_method)

        url_helpers.alias_method(unscoped_method, existing_method)
      end

      def self.current_organization
        # rubocop:disable Gitlab/AvoidCurrentOrganization -- Current organization not available earlier.
        Current.organization_assigned && Current.organization
        # rubocop:enable Gitlab/AvoidCurrentOrganization
      end

      def self.find_route_pairs
        all_routes = Rails.application.routes.routes
        org_routes, global_routes = all_routes.partition { |route| organization_route?(route) }
        build_route_pairs(org_routes, global_routes)
      end

      # Route name represents an Organization route.
      def self.organization_route?(route)
        route.path.spec.to_s.include?(ORGANIZATION_PATH_PATTERN)
      end

      # Build a Hash of global route => Organization route names.
      def self.build_route_pairs(organization_routes, global_routes)
        org_route_names = organization_routes.map(&:name)
        global_route_names = global_routes.map(&:name)

        # Global route => Organization route
        org_route_names.each_with_object({}) do |org_route_name, route_pairs|
          global_route_name = extract_global_route_name(org_route_name)
          next unless global_route_names.include?(global_route_name)

          route_pairs[global_route_name] = org_route_name
        end
      end

      # Map organization named route to global route.
      def self.extract_global_route_name(org_route_name)
        return if org_route_name.nil?

        # Strip only the first `organization_` token (the `as: :organization`
        # scope prefix); `gsub` would also remove a legitimate `organization_`
        # in the global name, e.g. the admin `organization_dashboard`.
        org_route_name.sub(ORGANIZATION_PATH_REGEX, '')
      end

      # Build a module that overrides URL helpers with organization-aware versions
      def self.build_override_module(route_pairs)
        Module.new do
          route_pairs.each do |global_route, org_route|
            [PATH_SUFFIX, URL_SUFFIX].each do |suffix|
              method_name = "#{global_route}#{suffix}"
              org_method_name = "#{org_route}#{suffix}"

              define_method(method_name) do |*args, **kwargs|
                current_organization = Routing::OrganizationsHelper::MappedHelpers.current_organization

                # Handle Ruby 2.4+ keyword argument compatibility
                # If kwargs is empty but last arg is a hash, treat it as kwargs
                kwargs = args.pop if kwargs.empty? && args.last.is_a?(Hash) && !args.last.frozen?

                if current_organization && current_organization.scoped_paths?
                  kwargs[:organization_path] ||= current_organization.path
                end

                if kwargs[:organization_path]
                  # Call the Organization helper method
                  method(org_method_name).call(*args, **kwargs)
                else
                  # Call the original helper method
                  super(*args, **kwargs)
                end
              end
            end
          end
        end
      end
    end

    included do
      Rails.application.config.after_routes_loaded do
        MappedHelpers.install
      end
    end
  end
end
