#!/usr/bin/env ruby
#
# Print a report on our Gemfile
# Why not just use `bundle outdated`? It doesn't give us the information we care about (and it fails).
#
at_exit do
  require "optparse"

  options = {}
  option_parser = OptionParser.new do |opts|
    opts.banner = <<~EOS
      Usage: #{$0} [report-type] [options]

      report-type  There are two report types available: `outdated` and `compatibility`

      Examples:
        #{$0} compatibility --rails-version 5.0
        #{$0} outdated
    EOS

    opts.separator ""
    opts.separator "Options:"

    opts.on("--rails-version [STRING]", "Rails version to check compatibility against (defaults to 5.0)") do |rails_version|
      options[:rails_version] = rails_version
    end

    opts.on("--include-rails-gems", "Inlcude Rails gems in compatibility report (defaults to false)") do
      options[:include_rails_gems] = true
    end

    opts.on_tail("-h", "--help", "Show this message") do
      puts opts
      exit
    end
  end

  begin
    option_parser.parse!
  rescue OptionParser::ParseError => e
    STDERR.puts e.message.red
    puts option_parser
    exit 1
  end

  report_type = ARGV.first

  case report_type
  when "outdated" then BundleReport.outdated
  else
    BundleReport.compatibility(rails_version: options.fetch(:rails_version, "5.0"), include_rails_gems: options.fetch(:include_rails_gems, false))
  end
end

# Needs to happen first
require "bundler/setup"

require "action_view"
require "active_support/core_ext/object/acts_like"
require "colorize"
require "cgi"
require "erb"
require "json"
require "rest-client"

class BundleReport
  def self.compatibility(rails_version:, include_rails_gems:)
    incompatible_gems = BundleReport::GemInfo.all.reject do |gem|
      gem.compatible_with_rails?(rails_version: rails_version) || (!include_rails_gems && gem.from_rails?)
    end.sort_by do |gem|
      [
        gem.latest_version.compatible_with_rails?(rails_version: rails_version) ? 0 : 1,
        gem.name
      ].join("-")
    end

    incompatible_gems_by_state = incompatible_gems.group_by { |gem| gem.state(rails_version) }

    template = <<~ERB
      <% if incompatible_gems_by_state[:latest_compatible] -%>
      <%= "=> Incompatible with Rails #{rails_version} (with new versions that are compatible):".white.bold %>
      <%= "These gems will need to be upgraded before upgrading to Rails #{rails_version}.".italic %>

      <% incompatible_gems_by_state[:latest_compatible].each do |gem| -%>
      <%= gem_header(gem) %> - upgrade to <%= gem.latest_version.version %>
      <% end -%>

      <% end -%>
      <% if incompatible_gems_by_state[:incompatible] -%>
      <%= "=> Incompatible with Rails #{rails_version} (with no new compatible versions):".white.bold %>
      <%= "These gems will need to be removed or replaced before upgrading to Rails #{rails_version}.".italic %>

      <% incompatible_gems_by_state[:incompatible].each do |gem| -%>
      <%= gem_header(gem) %> - new version, <%= gem.latest_version.version %>, is not compatible with Rails #{rails_version}
      <% end -%>

      <% end -%>
      <% if incompatible_gems_by_state[:no_new_version] -%>
      <%= "=> Incompatible with Rails #{rails_version} (with no new versions):".white.bold %>
      <%= "These gems will need to be upgraded by us or removed before upgrading to Rails #{rails_version}.".italic %>
      <%= "This list is likely to contain internal gems, like Cuddlefish.".italic %>

      <% incompatible_gems_by_state[:no_new_version].each do |gem| -%>
      <%= gem_header(gem) %> - new version not found
      <% end -%>

      <% end -%>
      <%= incompatible_gems.length.to_s.red %> gems incompatible with Rails <%= rails_version %>
    ERB

    puts ERB.new(template, nil, "-").result(binding)
  end

  def self.gem_header(_gem)
    header = "#{_gem.name} #{_gem.version}".bold
    header << " (loaded from git)".magenta if _gem.sourced_from_git?
    header
  end

  def self.outdated
    gems = BundleReport::GemInfo.all
    out_of_date_gems = gems.reject(&:up_to_date?).sort_by(&:created_at)
    percentage_out_of_date = ((out_of_date_gems.count / gems.count.to_f) * 100).round
    sourced_from_git = gems.select(&:sourced_from_git?)

    out_of_date_gems.each do |_gem|
      header = "#{_gem.name} #{_gem.version}"

      puts <<~MESSAGE
        #{header.bold.white}: released #{_gem.age} (latest version, #{_gem.latest_version.version}, released #{_gem.latest_version.age})
      MESSAGE
    end

    puts ""
    puts <<~MESSAGE
      #{"#{sourced_from_git.count}".yellow} gems are sourced from git
      #{"#{out_of_date_gems.length}".red} of the #{gems.count} gems are out-of-date (#{percentage_out_of_date}%)
    MESSAGE
  end

  class GemInfo
    include ActionView::Helpers::DateHelper

    class NullGemInfo < GemInfo
      def initialize; end

      def age
        "-"
      end

      def time_to_latest_version
        "-"
      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

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

    attr_reader :gem_specification, :version, :name
    def initialize(gem_specification)
      @gem_specification = gem_specification
      @version = gem_specification.version
      @name = gem_specification.name
    end

    def age
      "#{time_ago_in_words(created_at)} ago"
    end

    def sourced_from_git?
      !!gem_specification.git_version
    end

    def time_to_latest_version
      distance_of_time_in_words(created_at, latest_version.created_at)
    end

    def created_at
      @created_at ||= gem_specification.date
    end

    def up_to_date?
      version == latest_version.version
    end

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

    def latest_version
      @latest_version ||= begin
        latest_gem_specification = Gem.latest_spec_for(name)
        if latest_gem_specification
          GemInfo.new(latest_gem_specification)
        else
          NullGemInfo.new
        end
      end
    end

    def compatible_with_rails?(rails_version: Gem::Version.new("5.0"))
      unsatisfied_rails_dependencies(rails_version: rails_version).empty?
    end

    def unsatisfied_rails_dependencies(rails_version:)
      rails_dependencies = gem_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

    def from_rails?
      rails_gems.include?(name)
    end

    private def rails_gems
      [
        "rails",
        "activemodel",
        "activerecord",
        "actionmailer",
        "actioncable",
        "actionpack",
        "actionview",
        "activejob",
        "activestorage",
        "activesupport",
        "railties",
        "rails-dom-testing",
      ]
    end
  end
end
