require "inspec/cached_fetcher"
require "semverse"

module Inspec
  #
  # Inspec::Requirement represents a given profile dependency, where
  # appropriate we delegate to Inspec::Profile directly.
  #
  class Requirement
    def self.from_metadata(dep, cache, opts)
      raise "Cannot load empty dependency." if dep.nil? || dep.empty?

      req_path = opts[:cwd]

      if dep[:path]
        req_path = File.expand_path(dep[:path], req_path)
      end
      config = {
        cache: cache,
        cwd: req_path,
      }

      new(dep[:name],
          dep[:version],
          config,
          opts.merge(dep))
    end

    def self.from_lock_entry(entry, config, opts = {})
      resolved_source = entry[:resolved_source]
        .merge(backend: config[:backend])
        .merge(opts)

      req = new(entry[:name],
                entry[:version_constraints],
                config,
                resolved_source)

      locked_deps = []
      Array(entry[:dependencies]).each do |dep_entry|
        dep_config = config.dup
        dep_config[:parent_profile] = entry[:name]
        locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, dep_config, opts)
      end
      req.lock_deps(locked_deps)
      req
    end

    attr_reader :cwd, :opts, :version_constraints, :cache
    def initialize(name, version_constraints, config, opts)
      @name = name
      @version_constraints = Array(version_constraints)
      @cache = config[:cache]
      @backend = opts[:backend]
      @opts = opts
      @cwd = config[:cwd]
      @parent_profile = config[:parent_profile]
    end

    #
    # A dependency can be renamed in inspec.yml/inspec.lock.  Prefer
    # the name the user gave this dependency over the profile name.
    #
    def name
      @name || profile.name
    end

    def source_version
      profile.version
    end

    def source_satisfies_spec?
      return true if version_constraints.empty?

      # Semverse::Constraint.satisfy_all returns a list of versions that match all of the
      # supplied constraints. Since we're only matching against a single version, the return
      # of satisfy_all will be non-empty if the profile version we have satisfies the constraints.
      constraints = @version_constraints.map { |x| Semverse::Constraint.new(x) }
      !Semverse::Constraint.satisfy_all(constraints, Semverse::Version.new(profile.version)).empty?
    end

    def resolved_source
      @resolved_source ||= fetcher.resolved_source
    end

    def to_hash
      h = {
        "name" => name,
        "resolved_source" => resolved_source,
        "version_constraints" => version_constraints,
      }

      unless dependencies.empty?
        h["dependencies"] = dependencies.map(&:to_hash)
      end

      h
    end

    def lock_deps(dep_array)
      @dependencies = dep_array
    end

    def fetcher
      @runner_options ||= (Inspec::Config.cached || {})
      @fetcher ||= Inspec::CachedFetcher.new(opts, @cache, @runner_options)
    end

    # load dependencies of the dependency
    def dependencies
      @dependencies ||= profile.metadata.dependencies.map do |r|
        Inspec::Requirement.from_metadata(r, @cache, cwd: @cwd, backend: @backend)
      end
    end

    def to_s
      name
    end

    # load the profile for the requirement
    def profile
      return @profile unless @profile.nil?

      opts = @opts.dup
      opts[:backend] = @backend
      opts[:runner_conf] = Inspec::Config.cached
      if !@dependencies.nil? && !@dependencies.empty?
        opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @cache, @backend)
      end
      opts[:profile_name] = @name
      opts[:parent_profile] = @parent_profile
      @profile = Inspec::Profile.for_fetcher(fetcher, opts)
      @profile
    end
  end
end
