module NextRails
  class GemInfo
    class NullGemInfo < GemInfo
      def initialize; end

      def age
        "-"
      end

      def created_at
        Time.now
      end

      def up_to_date?
        false
      end

      def version
        "NOT FOUND"
      end

      def unsatisfied_rails_dependencies(*)
        ["unknown"]
      end

      def state(_)
        :null
      end
    end

    RAILS_GEMS = [
      "rails",
      "activemodel",
      "activerecord",
      "actionmailer",
      "actioncable",
      "actionpack",
      "actionview",
      "activejob",
      "activestorage",
      "activesupport",
      "railties",
    ].freeze

    def self.all
      Gem::Specification.each.map do |gem_specification|
        new(gem_specification)
      end
    end

    attr_reader :gem_specification, :version, :name, :latest_compatible_version

    def initialize(gem_specification)
      @gem_specification = gem_specification
      @version = gem_specification.version
      @name = gem_specification.name
    end

    def age
      created_at.strftime("%b %e, %Y")
    end

    def sourced_from_git?
      !!gem_specification.git_version
    end

    def created_at
      @created_at ||= gem_specification.date
    end

    def up_to_date?
      version == latest_version.version
    end

    def from_rails?
      RAILS_GEMS.include?(name)
    end

    def state(rails_version)
      if compatible_with_rails?(rails_version: rails_version)
        :compatible
      elsif latest_compatible_version && latest_compatible_version.version == "NOT FOUND"
        :no_new_version
      elsif latest_compatible_version
        :found_compatible
      else
        :incompatible
      end
    end

    def latest_version
      latest_gem_specification = Gem.latest_spec_for(name)
      return NullGemInfo.new unless latest_gem_specification

      GemInfo.new(latest_gem_specification)
    rescue
      NullGemInfo.new
    end

    def compatible_with_rails?(rails_version: nil)
      unsatisfied_rails_dependencies(rails_version: rails_version).empty?
    end

    def unsatisfied_rails_dependencies(rails_version: nil)
      spec_compatible_with_rails?(specification: gem_specification, rails_version: rails_version)
    end

    def find_latest_compatible(rails_version: nil)
      dependency = Gem::Dependency.new(@name)
      fetcher = Gem::SpecFetcher.new

      # list all available data for released gems
      list, errors = fetcher.available_specs(:released)

      specs = []
      # filter only specs for the current gem and older versions
      list.each do |source, gem_tuples|
        gem_tuples.each do |gem_tuple|
          if gem_tuple.name == @name && gem_tuple.version > @version
            specs << source.fetch_spec(gem_tuple)
          end
        end
      end

      # if nothing is found, consider gem incompatible
      if specs.empty?
        @latest_compatible_version = NullGemInfo.new
        return
      end

      # if specs are found, look for the first one from that is compatible
      # with the desired rails version starting from the end
      specs.reverse.each do |spec|
        if spec_compatible_with_rails?(specification: spec, rails_version: rails_version).empty?
          @latest_compatible_version = spec
          break
        end
      end
    end

    def spec_compatible_with_rails?(specification: nil, rails_version: nil)
      rails_dependencies = specification.runtime_dependencies.select {|dependency| RAILS_GEMS.include?(dependency.name) }

      rails_dependencies.reject do |rails_dependency|
        rails_dependency.requirement.satisfied_by?(Gem::Version.new(rails_version))
      end
    end
  end
end