require 'open3'
require 'yaml'
require 'gemika/errors'
require 'gemika/env'
require 'gemika/matrix/github_actions_config'

module Gemika
  class Matrix

    ##
    # A row in the test matrix
    #
    class Row

      def initialize(attrs)
        @ruby = attrs.fetch(:ruby)
        @gemfile = attrs.fetch(:gemfile)
      end

      ##
      # The Ruby version for the row.
      #
      attr_reader :ruby

      ##
      # The actually used Ruby version for the row.
      #
      attr_reader :used_ruby

      ##
      # The path to the gemfile for the row.
      #
      attr_reader :gemfile

      ##
      # Returns whether this row can be run with the given Ruby version.
      #
      def compatible_with_ruby?(current_ruby = Env.ruby)
        @used_ruby = aliased_ruby(ruby)

        @used_ruby == current_ruby
      end

      ##
      # Raises an error if this row is invalid.
      #
      # @!visibility private
      #
      def validate!
        File.exist?(gemfile) or raise MissingGemfile, "Gemfile not found: #{gemfile}"
        contents = File.read(gemfile)
        contents.include?('gemika') or raise UnusableGemfile, "Gemfile is missing gemika dependency: #{gemfile}"
      end

      private

      ##
      # Checks if the requested ruby version is aliased by rbenv to use another ruby version.
      # Returns the runnable ruby version.
      #
      def aliased_ruby(requested_version)
        ruby_aliases = rbenv_aliases

        aliased_versions = {}

        ruby_aliases.split("\n").each do |ruby_alias|
          split_pattern = /\A(.+) => (.+)\z/
          alias_name, aliased_version = ruby_alias.match(split_pattern)&.captures
          aliased_versions[alias_name] = aliased_version
        end

        find_aliased_ruby(requested_version, aliased_versions)
      end

      ##
      # Recursively traverses aliases until the requested Ruby version is found.
      # Returns the requested version if no alias can be found for that version.
      #
      def find_aliased_ruby(requested_version, aliased_versions)
        found_version = aliased_versions[requested_version]

        if found_version == requested_version
          found_version
        elsif found_version
          find_aliased_ruby(found_version, aliased_versions)
        else
          requested_version
        end
      end

      ##
      # Returns the list of rbenv aliases, if rbenv is installed with rbenv-aliases plugin.
      #
      def rbenv_aliases
        _output, status = Open3.capture2e('which', 'rbenv')
        return '' unless status.success?

        output, status = Open3.capture2e('rbenv', 'alias', '--list')
        if status.success?
          output
        else
          ''
        end
      end

    end

    COLOR_HEAD = "\e[44;97m"
    COLOR_WARNING = "\e[33m"
    COLOR_SUCCESS = "\e[32m"
    COLOR_FAILURE = "\e[31m"
    COLOR_RESET = "\e[0m"

    def initialize(options)
      @rows = options.fetch(:rows)
      @silent = options.fetch(:silent, false)
      @io = options.fetch(:io, STDOUT)
      @color = options.fetch(:color, true)
      validate = options.fetch(:validate, true)
      @rows.each(&:validate!) if validate
      @results = Env.new_ordered_hash
      @compatible_count = 0
      @all_passed = nil
      @current_ruby = options.fetch(:current_ruby, RUBY_VERSION)
      @aliased_rubys = {}
    end

    ##
    # Runs the given `block` for each matrix row that is compatible with the current Ruby.
    #
    # The row's gemfile will be set as an environment variable, so Bundler will use that gemfile if you shell out in `block`.
    #
    # At the end it will print a summary of which rows have passed, failed or were skipped (due to incompatible Ruby version).
    #
    def each(&block)
      @all_passed = true
      rows.each do |row|
        gemfile = row.gemfile
        if row.compatible_with_ruby?(current_ruby)
          @compatible_count += 1

          @aliased_rubys[current_ruby] = row.ruby

          print_title gemfile
          gemfile_passed = Env.with_gemfile(gemfile, row, &block)
          @all_passed &= gemfile_passed
          if gemfile_passed
            @results[row] = tint('Success', COLOR_SUCCESS)
          else
            @results[row] = tint('Failed', COLOR_FAILURE)
          end
        else
          @results[row] = tint("Skipped", COLOR_WARNING)
        end
      end
      print_summary
    end


    class << self
      ##
      # Builds a {Matrix} from the given Github Action workflow definition
      #
      # @param [Hash] options
      # @option path [String] Path to the `.yml` file.
      #
      def from_github_actions_yml(options = {})
        rows = GithubActionsConfig.load_rows(options)
        new(options.merge(rows: rows))
      end

      alias from_ci_config from_github_actions_yml
    end

    attr_reader :rows, :current_ruby

    private

    def puts(*args)
      unless @silent
        @io.puts(*args)
      end
    end

    def tint(message, color)
      if @color
        color + message + COLOR_RESET
      else
        message
      end
    end

    def print_title(title)
      puts
      puts tint(title, COLOR_HEAD)
      puts
    end

    def print_summary
      print_title 'Summary'

      gemfile_size = @results.keys.map { |row| row.gemfile.size }.max
      ruby_size = @results.keys.map { |row| row.ruby.size }.max

      @results.each do |entry, result|
        puts "- #{entry.gemfile.ljust(gemfile_size)}  Ruby #{entry.ruby.ljust(ruby_size)}  #{result}"
      end

      puts

      if @compatible_count == 0
        message = "No gemfiles were compatible with Ruby #{@aliased_rubys[RUBY_VERSION]}"
        puts tint(message, COLOR_FAILURE)
        raise UnsupportedRuby, message
      elsif @all_passed
        puts tint("All gemfiles succeeded for Ruby #{@aliased_rubys[RUBY_VERSION]}", COLOR_SUCCESS)
      else
        message = 'Some gemfiles failed'
        puts tint(message, COLOR_FAILURE)
        puts
        raise MatrixFailed, message
      end

      print_aliases

      puts
    end

    def print_aliases
      @aliased_rubys.select { |used_version, alias_name| used_version != alias_name }.each do |used_version, alias_name|
        puts tint("Ruby #{alias_name} is an alias for Ruby #{used_version} in this environment.", COLOR_WARNING)
      end
    end

  end

end
