require_relative 'yaml/reader'
require_relative '../services/services_helper'
require_relative 'service_generator_v2'
require_relative '../../stacks_client'
require_relative 'yaml/stack_file_loader'
require 'yaml'

module Kontena::Cli::Stacks
  module Common
    include Kontena::Cli::Services::ServicesHelper

    # @return [StackFileLoader] a loader for the stack origin defined through command-line options
    def loader
      @loader ||= loader_class.for(source)
    end

    # @return [YAML::Reader] a YAML reader for the target file
    def reader
      @reader ||= loader.reader
    end

    # Stack name read from -n parameter or the stack file
    # @return [String]
    def stack_name
      @stack_name ||= (self.respond_to?(:name) && self.name) ? self.name : loader.stack_name.stack
    end

    # An accessor to the YAML Reader outcome. Passes parent name, values from command line and
    # the stackname to the reader.
    #
    # @return [Hash]
    def stack
      @stack ||= reader.execute(
        name: stack_name,
        parent_name: self.respond_to?(:parent_name) ? self.parent_name : nil,
        values: (self.respond_to?(:values_from_options) ? self.values_from_options : {})
      )
    end

    # @return [Class] an accessor to StackFileLoader constant, for testing purposes
    def loader_class
      ::Kontena::Cli::Stacks::YAML::StackFileLoader
    end

    module RegistryNameParam
      def stack_name
        @stack_name ||= Kontena::Cli::Stacks::StackName.new(source)
      end

      def self.included(where)
        where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version", attribute_name: :source
      end
    end

    module StackNameParam
      # Include to add a STACK_NAME parameter
      def self.included(where)
        where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version", attribute_name: :source
      end
    end

    module StackFileOrNameParam
      # Include to add a stack file parameter
      def self.included(where)
        where.parameter "[FILE]", "Kontena stack file, registry stack name (user/stack or user/stack:version) or URL", default: "kontena.yml", attribute_name: :source
      end
    end

    module StackNameOption
      # Include to add a stack name parameter
      def self.included(where)
        where.option ['-n', '--name'], 'NAME', 'Define stack name (by default comes from stack file)'
      end
    end

    module StackValuesToOption
      attr_accessor :values
      # Include to add --values-to variable value dumping feature
      def self.included(where)
        where.option '--values-to', '[FILE]', 'Output variable values as YAML to file'
      end

      # Writes a YAML file from the values received from YAML::Reader to a file defined through
      # the --values-to option
      def dump_variables
        File.write(values_to, ::YAML.dump(reader.variable_values, without_defaults: true, without_vault: true))
      end
    end

    module StackValuesFromOption
      # Include to add --values-from option to read variable values from a YAML file
      # and the -v variable=value option that can be used to pass variable values
      # directly from command line
      def self.included(where)
        where.prepend InstanceMethods

        where.option '--values-from', '[FILE]', 'Read variable values from a YAML file', multivalued: true do |filename|
          values_from_file.merge!(::YAML.safe_load(File.read(filename), [], [], true, filename))
          filename
        end

        where.option '--values-from-stack', '[STACK_NAME]', 'Read variable values from an installed stack', multivalued: true do |stackname|
          variables = read_values_from_stacks(stackname)
          Kontena.logger.debug { "Received variables from stack #{stackname} on Master: #{variables.inspect}" }
          warn "Stack #{stackname} does not have any values for variables" if variables.empty?
          values_from_installed_stacks.merge!(variables)
          stackname
        end

        where.option '-v', "VARIABLE=VALUE", "Set stack variable values, example: -v domain=example.com. Can be used multiple times.", multivalued: true, attribute_name: :var_option do |var_pair|
          var_name, var_value = var_pair.split('=', 2)
          values_from_value_options.merge!(::YAML.safe_load(::YAML.dump(var_name => var_value)))
          var_pair
        end
      end

      module InstanceMethods
        def read_values_from_stacks(stackname)
          result = {}
          response = client.get("stacks/#{current_grid}/#{stackname}")
          result.merge!(response['variables']) if response['variables']
          if response['children']
            response['children'].each do |child_info|
              result.merge!(
                read_values_from_stacks(child_info['name']).tap do |child_result|
                  child_result.keys.each do |key|
                    new_key = child_info['name'].dup # foofoo-redis-monitor
                    new_key.sub!("#{stackname}-", '') # monitor
                    new_key.concat ".#{key}" # monitor.foovariable
                    child_result[new_key] = child_result.delete(key)
                  end
                end
              )
            end
          end
          result
        end

        def values_from_file
          @values_from_file ||= {}
        end

        def values_from_value_options
          @values_from_value_options ||= {}
        end

        def values_from_installed_stacks
          @values_from_installed_stacks ||= {}
        end

        def values_from_options
          @values_from_options ||= values_from_installed_stacks.merge(values_from_file).merge(values_from_value_options)
        end

        # Transforms a hash
        # dependency_values_from_options('foo.bar' => 1, 'foo')
        #  => { 'bar' => 1 }
        # Used for dependency variable injection
        def dependency_values_from_options(name)
          name_with_dot = name.to_s + '.'
          values_from_options.each_with_object({}) do |kv_pair, obj|
            key = kv_pair.first.to_s
            value = kv_pair.last
            next unless key.start_with?(name_with_dot)
            obj[key.sub(name_with_dot, '')] = value
          end
        end
      end
    end

    # Sets environment variables from parameters
    # @param stack [String] current stack name
    # @param grid [String] current grid name
    # @param platform [String] current platform name, defaults to param grid value
    def set_env_variables(stack, grid, platform = grid)
      ENV['STACK'] = stack
      ENV['GRID'] = grid
      ENV['PLATFORM'] = platform
    end

    # @return [String]
    def current_dir
      File.basename(Dir.getwd)
    end

    def display_notifications(messages, color = :yellow)
      $stderr.puts(pastel.send(color, messages.to_yaml.gsub(/^---$/, '')))
    end

    def hint_on_validation_notifications(notifications, filename = nil)
      return if notifications.nil? || notifications.empty?
      $stderr.puts pastel.yellow("#{"(#{filename}) " if filename}YAML contains the following unsupported options and they were rejected:")
      display_notifications(notifications)
    end

    def abort_on_validation_errors(errors, filename = nil)
      return if errors.nil? || errors.empty?
      $stderr.puts pastel.red("#{"(#{filename}) " if filename} YAML validation failed! Aborting.")
      display_notifications(errors, :red)
      abort
    end

    # An accessor to stack registry client
    # @return [Kontena::StacksClient]
    def stacks_client
      @stacks_client ||= Kontena::StacksClient.new(current_account.stacks_url, current_account.token, read_requires_token: current_account.stacks_read_authentication)
    end
  end
end
