require 'wright/config'
require 'wright/util'
require 'wright/logger'
require 'wright/dry_run'

module Wright
  # Resource base class.
  class Resource
    # Initializes a Resource.
    #
    # @param name [String] the name of the resource
    def initialize(name = nil)
      @name = name
      @resource_name = Util.class_to_resource_name(self.class).to_sym
      @provider = provider_for_resource
      @action = nil
      @on_update = nil
      @ignore_failure = false
    end

    # @return [Symbol] the name of the method to be run by {#run_action}
    attr_accessor :action

    # @return [Bool] the ignore_failure attribute
    attr_accessor :ignore_failure

    # @return [String] the resource's name attribute
    #
    # @example
    #   foo = Wright::Resource::Symlink.new('/tmp/fstab')
    #   foo.name
    #   # => "/tmp/fstab"
    #
    #   bar = Wright::Resource::Symlink.new
    #   bar.name = '/tmp/passwd'
    #   bar.name
    #   # => "/tmp/passwd"
    attr_accessor :name

    # @return [Symbol] a compact resource name
    #
    # @example
    #   foo = Wright::Resource::Symlink.new
    #   foo.resource_name
    #   # => :symlink
    attr_reader :resource_name

    # Sets an update action for a resource.
    #
    # @param on_update [Proc, #call] the block that is called when the
    #   resource is updated.
    #
    # @return [void]
    # @raise [ArgumentError] if on_update is not callable
    def on_update=(on_update)
      if on_update.respond_to?(:call) || on_update.nil?
        @on_update = on_update
      else
        fail ArgumentError, "#{on_update} is not callable"
      end
    end

    # Runs the resource's current action.
    #
    # @example
    #   fstab = Wright::Resource::Symlink.new('/tmp/fstab')
    #   fstab.action = :remove
    #   fstab.run_action
    #
    # @return the return value of the current action
    def run_action
      send @action if @action
    end

    private

    # @api public
    # Marks a code block that might update a resource.
    #
    # Usually this method is called in the definition of a new
    # resource class in order to mark those methods that should be
    # able to trigger update actions. Runs the current update action
    # if the provider was updated by the block method.
    #
    # @example
    #   class BalloonAnimal < Wright::Provider
    #     def inflate
    #       puts "It's a giraffe!"
    #       @updated = true
    #     end
    #   end
    #
    #   class Balloon < Wright::Resource
    #     def inflate
    #       might_update_resource { @provider.inflate }
    #     end
    #   end
    #   Wright::Config[:resources] = { balloon: { provider: 'BalloonAnimal' } }
    #
    #   balloon = Balloon.new.inflate
    #   # => true
    #
    # @return [Bool] true if the provider was updated and false
    #   otherwise
    def might_update_resource
      begin
        yield
      rescue => e
        log_error(e)
        raise e unless @ignore_failure
      end
      updated = @provider.updated?
      run_update_action if updated
      updated
    end

    def log_error(exception)
      resource = "#{@resource_name}"
      resource << " '#{@name}'" if @name
      Wright.log.error "#{resource}: #{exception}"
    end

    def run_update_action
      return if @on_update.nil?

      resource = "#{@resource_name} '#{@name}'"
      notification = "run update action for #{resource}"
      if Wright.dry_run?
        Wright.log.info "(would) #{notification}"
      else
        Wright.log.info notification
        @on_update.call
      end
    end

    def resource_class
      Util::ActiveSupport.camelize(@resource_name)
    end

    def provider_name
      if Wright::Config.nested_key?(:resources, @resource_name, :provider)
        Wright::Config[:resources][@resource_name][:provider]
      else
        "Wright::Provider::#{resource_class}"
      end
    end

    def provider_for_resource
      klass = Util::ActiveSupport.safe_constantize(provider_name)
      if klass
        klass.new(self)
      else
        warning = "Could not find a provider for resource #{resource_class}"
        Wright.log.warn warning
        nil
      end
    end
  end
end