# frozen_string_literal: true

require "fast_blank"
require "pathname"
require "dotenv_validator/errors"

# Knows how to check the environment variables against the comments defined
# in every line of your .env.sample or .env.template file.
module DotenvValidator
  # It analyzes the current environment and it compares it to the documentation
  # present in .env.sample or .env.template.
  #
  # @return [[String],[String]] An array with two arrays. First array: List of missing variables. Second array: List of variables with invalid format.
  def self.analyze_variables
    return [], [] unless File.exist?(sample_file)

    missing_variables = []
    invalid_format = []

    open_reference_file.each do |line|
      variable, config = line.split(" #")
      variable_name, _sample = variable.split("=")
      value = ENV[variable_name]

      if value.nil? || value.blank?
        missing_variables << variable_name if config.to_s.match?(/required/)
        next
      end

      next unless config =~ /format=(.*)/

      format = Regexp.last_match(1)

      valid =
        case format
        when "int", "integer", "Integer" then integer?(value)
        when "float", "Float" then float?(value)
        when "str", "string", "String" then true
        when "email" then email?(value)
        when "url" then url?(value)
        when "bool", "boolean", "Boolean" then boolean?(value)
        when "uuid", "UUID" then uuid?(value)
        else
          value.match?(Regexp.new(Regexp.last_match(1)))
        end

      invalid_format << {name: variable_name, value: value, format: format} unless valid
    end

    [missing_variables, invalid_format]
  end

  # It checks the current environment and it returns a boolean value.
  #
  # @return [Boolean] True if everything looks good. False otherwise.
  def self.check
    result = true

    missing_variables, invalid_format = analyze_variables
    if missing_variables.any?
      puts("WARNING - Missing environment variables: #{missing_variables.join(", ")}")
      result = false
    end

    if invalid_format.any?
      puts "WARNING - Environment variables with invalid format:"
      puts invalid_format_list(invalid_format)
      result = false
    end

    result
  end

  # It checks the current environment and it raises a runtime exception.
  #
  # @raise [RuntimeError] Raised if a missing variable is found or an invalid format is encountered.
  def self.check!
    missing_variables, invalid_format = analyze_variables

    raise("Missing environment variables: #{missing_variables.join(", ")}") if missing_variables.any?

    raise("Environment variables with invalid format:\n#{invalid_format_list(invalid_format)}") if invalid_format.any?
  end

  # It checks if the value is float or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is a float value. False otherwise.
  def self.float?(string)
    true if Float(string)
  rescue
    false
  end

  # It checks if the value is an integer or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is an integer value. False otherwise.
  def self.integer?(string)
    true if Integer(string)
  rescue
    false
  end

  # It checks if the value is an email or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is an email value. False otherwise.
  def self.email?(string)
    string.match?(URI::MailTo::EMAIL_REGEXP)
  end

  # It checks if the value is a URL or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is an URL value. False otherwise.
  def self.url?(string)
    string.match?(/\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/)
  end

  # It checks if the value is a boolean or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is a boolean value. False otherwise.
  def self.boolean?(string)
    string.match?(/(true|false)/)
  end

  # It checks if the value is a uuid or not.
  #
  # @param [String] A string
  # @return [Boolean] True if it is a UUID value. False otherwise.
  def self.uuid?(string)
    string.match?(/\A[\da-f]{32}\z/i) ||
      string.match?(/\A[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\z/i)
  end

  def self.open_reference_file
    sample_file
  rescue Errno::ENOENT
    begin
      template_file
    rescue Errno::ENOENT
      raise DotenvValidator::SampleFileNotFoundError, "Neither .env.sample nor .env.template files found!"
    end
  end

  def self.sample_file
    File.open(File.join(root, ".env.sample"))
  end

  def self.template_file
    File.open(File.join(root, ".env.template"))
  end

  # Internal: `Rails.root` is nil in Rails 4.1 before the application is
  # initialized, so this falls back to the `RAILS_ROOT` environment variable,
  # or the current working directory.
  #
  # Taken from Dotenv source code.
  def self.root
    root_or_pwd = Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd)

    if defined?(Rails)
      Rails.root || root_or_pwd
    else
      root_or_pwd
    end
  end

  def self.invalid_format_list(invalid_format)
    invalid_format.map do |var|
      %(- #{var[:name]}: expected "#{var[:value]}" to match "#{var[:format]}" format)
    end.join("\n")
  end
end
