require 'shellwords'

require 'nugrant/bag'
require 'nugrant/helper/env/namer'

module Nugrant
  module Helper
    module Env
      module Exporter
        DEFAULT_AUTOENV_PATH = "./.env"
        DEFAULT_SCRIPT_PATH = "./nugrant2env.sh"

        VALID_EXPORTERS = [:autoenv, :script, :terminal]

        ##
        # Returns true if the exporter name received is a valid
        # valid export, false otherwise.
        #
        # @param exporter The exporter name to check validity
        #
        # @return true if exporter is valid, false otherwise.
        def self.valid?(exporter)
          VALID_EXPORTERS.include?(exporter)
        end

        ##
        # Creates an autoenv script containing the commands that are required
        # to export or unset a bunch of environment variables taken from the
        # bag.
        #
        # @param bag The bag to create the script for.
        #
        # @return (side-effect) Creates a script file containing commands
        #                       to export or unset environment variables for
        #                       bag.
        #
        # Options:
        #  * :autoenv_path => The path where to write the script, defaults to `./.env`.
        #  * :escape_value => If true, escape the value to export (or unset), default to true.
        #  * :io => The io where the command should be written, default to nil which create the autoenv on disk.
        #  * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
        #  * :override => If true, variable a exported even when the override an existing env key, default to true.
        #  * :type => The type of command, default to :export.
        #
        def self.autoenv_exporter(bag, options = {})
          io = options[:io] || (File.open(File.expand_path(options[:autoenv_path] || DEFAULT_AUTOENV_PATH), "w"))

          terminal_exporter(bag, options.merge({:io => io}))
        ensure
          io.close() if io
        end

        ##
        # Creates a bash script containing the commands that are required
        # to export or unset a bunch of environment variables taken from the
        # bag.
        #
        # @param bag The bag to create the script for.
        #
        # @return (side-effect) Creates a script file containing commands
        #                       to export or unset environment variables for
        #                       bag.
        #
        # Options:
        #  * :escape_value => If true, escape the value to export (or unset), default to true.
        #  * :io => The io where the command should be written, default to nil which create the script on disk.
        #  * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
        #  * :override => If true, variable a exported even when the override an existing env key, default to true.
        #  * :script_path => The path where to write the script, defaults to `./nugrant2env.sh`.
        #  * :type => The type of command, default to :export.
        #
        def self.script_exporter(bag, options = {})
          io = options[:io] || (File.open(File.expand_path(options[:script_path] || DEFAULT_SCRIPT_PATH), "w"))

          io.puts("#!/bin/env sh")
          io.puts()

          terminal_exporter(bag, options.merge({:io => io}))
        ensure
          io.close() if io
        end

        ##
        # Export to terminal the commands that are required
        # to export or unset a bunch of environment variables taken from the
        # bag.
        #
        # @param bag The bag to create the script for.
        #
        # @return (side-effect) Outputs to io the commands generated.
        #
        # Options:
        #  * :escape_value => If true, escape the value to export (or unset), default to true.
        #  * :io => The io where the command should be displayed, default to $stdout.
        #  * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
        #  * :override => If true, variable a exported even when the override an existing env key, default to true.
        #  * :type => The type of command, default to :export.
        #
        def self.terminal_exporter(bag, options = {})
          io = options[:io] || $stdout
          type = options[:type] || :export

          export(bag, options) do |key, value|
            io.puts(command(type, key, value, options))
          end
        end

        ##
        # Generic function to export a bag. This walk the bag,
        # for each element, it creates the key using the namer
        # and then forward the key and value to the block if
        # the variable does not override an existing environment
        # variable or if options :override is set to true.
        #
        # @param bag The bag to export.
        #
        # @return (side-effect) Yields each key and value to a block
        #
        # Options:
        #  * :namer => The namer used to transform bag parents into variable name, default to Namer::default().
        #  * :override => If true, variable a exported even when the override an existing env key, default to true.
        #
        def self.export(bag, options = {})
          namer = options[:namer] || Env::Namer.default()
          override = options.fetch(:override, true)

          variables = {}
          bag.walk do |path, key, value|
            key = namer.call(path)

            variables[key] = value if override or not ENV[key]
          end

          variables.sort().each do |key, value|
            yield key, value
          end
        end

        ##
        # Given a key and a value, return a string representation
        # of the command type requested. Available types:
        #
        #  * :export => A bash compatible export command
        #  * :unset => A bash compatible export command
        #
        def self.command(type, key, value, options = {})
          # TODO: Replace by a map type => function name
          case
          when type == :export
            export_command(key, value, options)
          when type == :unset
            unset_command(key, value, options)
          end
        end

        ##
        # Returns a string representation of the command
        # that needs to be used on the current platform
        # to export an environment variable.
        #
        # @param key The key of the environment variable to export.
        #            It cannot be nil.
        # @param value The value of the environment variable to export
        #
        # @return The export command, as a string
        #
        # Options:
        #  * :escape_value (true) => If true, escape the value to export.
        #
        def self.export_command(key, value, options = {})
          value = value.to_s()
          value = Shellwords.escape(value) if options[:escape_value] == nil || options[:escape_value]

          # TODO: Handle platform differently
          "export #{key}=#{value}"
        end

        ##
        # Returns a string representation of the command
        # that needs to be used on the current platform
        # to unset an environment variable.
        #
        # @param key The key of the environment variable to export.
        #            It cannot be nil.
        #
        # @return The unset command, as a string
        #
        def self.unset_command(key, value, options = {})
          # TODO: Handle platform differently
          "unset #{key}"
        end
      end
    end
  end
end