# frozen_string_literal: true

require 'open-uri'
require 'license_finder/license'
require 'license_finder/manual_licenses'

module LicenseFinder
  class Decisions
    ######
    # READ
    ######

    attr_reader :packages, :permitted, :restricted, :ignored, :ignored_groups, :project_name, :inherited_decisions

    def licenses_of(name, version = nil)
      @manual_licenses.licenses_of(name, version)
    end

    def homepage_of(name)
      @homepages[name]
    end

    def approval_of(name, version = nil)
      if !@approvals.key?(name)
        nil
      elsif !version.nil?
        @approvals[name] if @approvals[name][:safe_versions].empty? || @approvals[name][:safe_versions].include?(version)
      elsif @approvals[name][:safe_versions].empty?
        @approvals[name]
      end
    end

    def approved?(name, version = nil)
      if !@approvals.key?(name)
        nil
      elsif !version.nil?
        @approvals.key?(name) && @approvals[name][:safe_versions].empty? || @approvals[name][:safe_versions].include?(version)
      else
        @approvals.key?(name)
      end
    end

    def permitted?(lic)
      if @permitted.include?(lic)
        true
      elsif lic.is_a?(OrLicense)
        lic.sub_licenses.any? { |sub_lic| @permitted.include?(sub_lic) }
      elsif lic.is_a?(AndLicense)
        lic.sub_licenses.all? { |sub_lic| @permitted.include?(sub_lic) }
      else
        false
      end
    end

    def restricted?(lic)
      @restricted.include?(lic)
    end

    def ignored?(name)
      @ignored.include?(name)
    end

    def ignored_group?(name)
      @ignored_groups.include?(name)
    end

    #######
    # WRITE
    #######

    TXN = Struct.new(:who, :why, :safe_when, :safe_versions) do
      def self.from_hash(txn, versions)
        new(txn[:who], txn[:why], txn[:when], versions || [])
      end
    end

    def initialize
      @decisions = []
      @packages = Set.new
      @manual_licenses = ManualLicenses.new
      @homepages = {}
      @approvals = {}
      @permitted = Set.new
      @restricted = Set.new
      @ignored = Set.new
      @ignored_groups = Set.new
      @inherited_decisions = Set.new
    end

    def add_package(name, version, txn = {})
      add_decision [:add_package, name, version, txn]
      @packages << ManualPackage.new(name, version)
      self
    end

    def remove_package(name, txn = {})
      add_decision [:remove_package, name, txn]
      @packages.delete(ManualPackage.new(name))
      self
    end

    def license(name, lic, txn = {})
      add_decision [:license, name, lic, txn]

      versions = txn[:versions]

      if versions.nil? || versions.empty?
        @manual_licenses.assign_to_all_versions(name, lic)
      else
        @manual_licenses.assign_to_specific_versions(name, lic, versions)
      end

      self
    end

    def unlicense(name, lic, txn = {})
      add_decision [:unlicense, name, lic, txn]

      versions = txn[:versions]

      if versions.nil? || versions.empty?
        @manual_licenses.unassign_from_all_versions(name, lic)
      else
        @manual_licenses.unassign_from_specific_versions(name, lic, versions)
      end

      self
    end

    def homepage(name, homepage, txn = {})
      add_decision [:homepage, name, homepage, txn]
      @homepages[name] = homepage
      self
    end

    def approve(name, txn = {})
      add_decision [:approve, name, txn]

      versions = []
      versions = @approvals[name][:safe_versions] if @approvals.key?(name)
      @approvals[name] = TXN.from_hash(txn, versions)
      @approvals[name][:safe_versions].concat(txn[:versions]) unless txn[:versions].nil?
      self
    end

    def unapprove(name, txn = {})
      add_decision [:unapprove, name, txn]
      @approvals.delete(name)
      self
    end

    def permit(lic, txn = {})
      add_decision [:permit, lic, txn]
      @permitted << License.find_by_name(lic)
      self
    end

    def unpermit(lic, txn = {})
      add_decision [:unpermit, lic, txn]
      @permitted.delete(License.find_by_name(lic))
      self
    end

    def restrict(lic, txn = {})
      add_decision [:restrict, lic, txn]
      @restricted << License.find_by_name(lic)
      self
    end

    def unrestrict(lic, txn = {})
      add_decision [:unrestrict, lic, txn]
      @restricted.delete(License.find_by_name(lic))
      self
    end

    def ignore(name, txn = {})
      add_decision [:ignore, name, txn]
      @ignored << name
      self
    end

    def heed(name, txn = {})
      add_decision [:heed, name, txn]
      @ignored.delete(name)
      self
    end

    def ignore_group(name, txn = {})
      add_decision [:ignore_group, name, txn]
      @ignored_groups << name
      self
    end

    def heed_group(name, txn = {})
      add_decision [:heed_group, name, txn]
      @ignored_groups.delete(name)
      self
    end

    def name_project(name, txn = {})
      add_decision [:name_project, name, txn]
      @project_name = name
      self
    end

    def unname_project(txn = {})
      add_decision [:unname_project, txn]
      @project_name = nil
      self
    end

    def inherit_from(filepath_info)
      decisions =
        case filepath_info
        when Hash
          resolve_inheritance(filepath_info)
        when %r{^https?://}
          open_uri(filepath_info).read
        else
          Pathname(filepath_info).read
        end

      add_decision [:inherit_from, filepath_info]
      @inherited_decisions << filepath_info
      restore_inheritance(decisions)
    end

    def resolve_inheritance(filepath_info)
      if (gem_name = filepath_info['gem'])
        Pathname(gem_config_path(gem_name, filepath_info['path'])).read
      else
        open_uri(filepath_info['url'], filepath_info['authorization']).read
      end
    end

    def gem_config_path(gem_name, relative_config_path)
      spec = Gem::Specification.find_by_name(gem_name)
      File.join(spec.gem_dir, relative_config_path)
    rescue Gem::LoadError => e
      raise Gem::LoadError,
            "Unable to find gem #{gem_name}; is the gem installed? #{e}"
    end

    def remove_inheritance(filepath)
      @decisions -= [[:inherit_from, filepath]]
      @inherited_decisions.delete(filepath)
      self
    end

    def add_decision(decision)
      @decisions << decision unless @inherited
    end

    def restore_inheritance(decisions)
      previous_value = @inherited
      @inherited = true
      self.class.restore(decisions, self)
      @inherited = previous_value
      self
    end

    def open_uri(uri, auth = nil)
      header = {}
      auth_header = resolve_authorization(auth)
      header['Authorization'] = auth_header if auth_header

      # ruby < 2.5.0 URI.open is private
      if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5.0')
        open(uri, header)
      else
        URI.open(uri, header)
      end
    end

    def resolve_authorization(auth)
      return unless auth

      token_env = auth.match(/\$(\S.*)/)
      return auth unless token_env

      token = ENV[token_env[1]]
      auth.sub(token_env[0], token)
    end

    #########
    # PERSIST
    #########

    def self.fetch_saved(file)
      restore(read!(file))
    end

    def save!(file)
      write!(persist, file)
    end

    def self.restore(persisted, result = new)
      return result unless persisted

      # From https://makandracards.com/makandra/465149-ruby-the-yaml-safe_load-method-hides-some-pitfalls
      actions = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1')
                  YAML.safe_load(persisted, permitted_classes: [Symbol, Time], aliases: true)
                else
                  YAML.safe_load(persisted, [Symbol, Time], [], true)
                end

      list_of_actions = (actions || []).map(&:first)

      if (list_of_actions & %i[whitelist blacklist]).any?
        raise 'The decisions file seems to have whitelist/blacklist keys which are deprecated. Please replace them with permit/restrict respectively and try again! More info - https://github.com/pivotal/LicenseFinder/commit/a40b22fda11b3a0efbb3c0a021381534bc998dd9'
      end

      (actions || []).each do |action, *args|
        result.send(action, *args)
      end
      result
    end

    def persist
      YAML.dump(@decisions)
    end

    def self.read!(file)
      file.read if file.exist?
    end

    def write!(value, file)
      file.dirname.mkpath
      file.open('w+') do |f|
        f.print value
      end
    end
  end
end
