require 'json'

module Stove
  class Cookbook
    # Borrowed and modified from:
    # {https://raw.github.com/chef/chef/11.4.0/lib/chef/cookbook/metadata.rb}
    #
    # Copyright:: Copyright 2008-2019 Chef Software, Inc.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # == Chef::Cookbook::Metadata
    # Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
    # about Chef Cookbooks.
    class Metadata
      class << self
        def from_file(path)
          new.from_file(path)
        end

        def def_attribute(field)
          class_eval <<-EOM, __FILE__, __LINE__ + 1
            def #{field}(arg = nil)
              set_or_return(:#{field}, arg)
            end
          EOM
        end

        def def_meta_cookbook(field, instance_variable)
          class_eval <<-EOM, __FILE__, __LINE__ + 1
            def #{field}(thing, *args)
              version = args.first
              @#{instance_variable}[thing] = version || DEFAULT_VERSION
              @#{instance_variable}[thing]
            end
          EOM
        end

        def def_meta_setter(field, instance_variable)
          class_eval <<-EOM, __FILE__, __LINE__ + 1
            def #{field}(name, description)
              @#{instance_variable}[name] = description
              @#{instance_variable}
            end
          EOM
        end

        def def_meta_gems(field, instance_variable)
          class_eval <<-EOM, __FILE__, __LINE__ + 1
            def #{field}(*args)
              @#{instance_variable} << args unless args.empty?
              @#{instance_variable}
            end
          EOM
        end

        def def_meta_version(field)
          class_eval <<-EOM, __FILE__, __LINE__ + 1
            def #{field}(*args)
              @#{field} << args unless args.empty?
              @#{field}
            end
          EOM
        end
      end

      DEFAULT_VERSION = '>= 0.0.0'.freeze

      COMPARISON_FIELDS = [
        :name, :description, :long_description, :maintainer,
        :maintainer_email, :license, :platforms, :dependencies,
        :recommendations, :suggestions, :conflicting, :providing,
        :replacing, :attributes, :groupings, :recipes, :version
      ]

      def_attribute :name
      def_attribute :maintainer
      def_attribute :maintainer_email
      def_attribute :license
      def_attribute :description
      def_attribute :long_description

      # These attributes are available for reading, but are not written by
      # default. In order to maintain backwards and forwards compatability,
      # these attributes are here.
      def_attribute :source_url
      def_attribute :issues_url
      def_meta_version :chef_version
      def_meta_version :ohai_version

      def_meta_cookbook :supports,   :platforms
      def_meta_cookbook :depends,    :dependencies
      def_meta_cookbook :recommends, :recommendations
      def_meta_cookbook :suggests,   :suggestions
      def_meta_cookbook :conflicts,  :conflicting
      def_meta_cookbook :provides,   :providing
      def_meta_cookbook :replaces,   :replacing

      def_meta_setter :recipe,    :recipes
      def_meta_setter :grouping,  :groupings
      def_meta_setter :attribute, :attributes
      def_meta_gems   :gem,       :gems

      attr_reader :cookbook
      attr_reader :platforms
      attr_reader :dependencies
      attr_reader :recommendations
      attr_reader :gems
      attr_reader :suggestions
      attr_reader :conflicting
      attr_reader :providing
      attr_reader :replacing
      attr_reader :attributes
      attr_reader :groupings
      attr_reader :recipes
      attr_reader :version

      def initialize(cookbook = nil, maintainer = 'YOUR_COMPANY_NAME', maintainer_email = 'YOUR_EMAIL', license = 'none')
        @cookbook         = cookbook
        @name             = cookbook ? cookbook.name : ''
        @long_description = ''
        @source_url       = Stove::Mash.new
        @issues_url       = Stove::Mash.new
        @gems             = []
        @chef_version     = []
        @ohai_version     = []
        @platforms        = Stove::Mash.new
        @dependencies     = Stove::Mash.new
        @recommendations  = Stove::Mash.new
        @suggestions      = Stove::Mash.new
        @conflicting      = Stove::Mash.new
        @providing        = Stove::Mash.new
        @replacing        = Stove::Mash.new
        @attributes       = Stove::Mash.new
        @groupings        = Stove::Mash.new
        @recipes          = Stove::Mash.new

        self.maintainer(maintainer)
        self.maintainer_email(maintainer_email)
        self.license(license)
        self.description('A fabulous new cookbook')
        self.version('0.0.0')

        if cookbook
          @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
            e = self.name if e =~ /::default$/
            r[e] = ""
            self.provides e
            r
          end
        end
      end

      def from_file(path)
        path = path.to_s
        path_json = File.join(File.dirname(path), 'metadata.json')

        if File.exist?(path) && File.readable?(path)
          self.instance_eval(IO.read(path), path, 1)
          self
        elsif File.exist?(path_json) && File.readable?(path_json)
          metadata_from_json(path_json)
        else
          raise Error::MetadataNotFound.new(path: path)
        end
      end

      def ==(other)
        COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
          equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
        end
      end

      def version(arg = UNSET_VALUE)
        if arg == UNSET_VALUE
          @version
        else
          @version = arg.to_s
        end
      end

      def to_hash(extended_metadata = false)
        hash = {
          'name'             => self.name,
          'version'          => self.version,
          'description'      => self.description,
          'long_description' => self.long_description,
          'maintainer'       => self.maintainer,
          'maintainer_email' => self.maintainer_email,
          'license'          => self.license,
          'platforms'        => self.platforms,
          'dependencies'     => self.dependencies,
          'recommendations'  => self.recommendations,
          'suggestions'      => self.suggestions,
          'conflicting'      => self.conflicting,
          'providing'        => self.providing,
          'replacing'        => self.replacing,
          'attributes'       => self.attributes,
          'groupings'        => self.groupings,
          'recipes'          => self.recipes,
        }

        if extended_metadata
          hash['source_url']   = self.source_url unless self.source_url.empty?
          hash['issues_url']   = self.issues_url unless self.issues_url.empty?
          hash['gems']         = self.gems unless self.gems.empty?
          hash['chef_version'] = self.chef_version.map(&:sort)
          hash['ohai_version'] = self.ohai_version.map(&:sort)
        end

        return hash
      end

      private

      def metadata_from_json(path)
        json = JSON.parse(IO.read(path))
        json.keys.each do |key|
          set_or_return(key.to_sym, json[key])
        end
        self
      end

      def set_or_return(symbol, arg)
        iv_symbol = "@#{symbol.to_s}".to_sym

        if arg.nil? && self.instance_variable_defined?(iv_symbol)
          self.instance_variable_get(iv_symbol)
        else
          self.instance_variable_set(iv_symbol, arg)
        end
      end
    end
  end
end
