require 'multi_json'
require 'virtus'

require "app_manifest/version"
require "app_manifest/nullable_array"
require "app_manifest/serializer"
require "app_manifest/addon"
require "app_manifest/buildpack"
require "app_manifest/env"
require "app_manifest/formation"
require "app_manifest/environment_attributes"
require "app_manifest/environment"
require "app_manifest/manifest"

# Create a new manifest from json string or a hash
def AppManifest(input)
  if input.is_a?(Hash)
    AppManifest::Manifest.new(input)
  elsif input.is_a?(String)
    AppManifest::Manifest.from_json(input)
  end
end

module AppManifest
  class << self
    # Takes a hash representing an app manifest and returns a new hash
    # with canonical serialization. This will resolve shorthands and older
    # serializations into a canonical serialization.
    def canonicalize(manifest)
      manifest = keys_to_sym(manifest)
      manifest
        .merge(canonicalize_env(manifest))
        .merge(canonicalize_formation(manifest))
        .merge(canonicalize_addons(manifest))
        .merge(canonicalize_environments(manifest))
    end

    private

    # Takes an env serialization returns a new serialization hash in the
    # canonical format.
    # For instance:
    # canonicalize_env({"FOO" => "BAR"}) # => { "FOO" => { value: "BAR" } }
    def canonicalize_env(manifest)
      canonicalize_key(manifest, :env) do |env|
        Hash[
          env.map do |key, value|
            case value
            when Hash
              [key.to_s, value]
            when String, TrueClass, FalseClass, Integer, Float
              [key.to_s, { value: value }]
            else
              [key.to_s, value.to_s]
            end
          end
        ]
      end
    end

    # Takes a formation serialization and returns a new serializaiton in the
    # standard format.
    # For example:
    # canonicalize_formation([{ "process" => "web", "count" => 1 }]
    # # => { web: { count: 1 } }
    def canonicalize_formation(manifest)
      canonicalize_key(manifest, :formation) do |formation|
        if formation.is_a? Array
          Hash[
            formation
            .map { |entry| keys_to_sym(entry) }
            .reject { |entry| entry[:process].to_s.empty? }
            .map do |entry|
              process = entry.fetch(:process)
              entry = entry.reject { |k, _| k == :process }
              [process.to_sym, entry]
            end
          ]
        else
          formation
        end
      end
    end

    # Takes an addon serialization and returns a new serialization in the
    # canonical format.
    # For example:
    # canonicalize_addons(["heroku-postgres:hobby-dev"])
    # # => { plan: "heroku-postgres:hobby-dev" }
    def canonicalize_addons(manifest)
      canonicalize_key(manifest, :addons) do |addons|
        addons.map do |entry|
          if entry.is_a? String
            {
              plan: entry,
            }
          else
            keys_to_sym(entry)
          end
        end
      end
    end


    # Takes an environments serialization and canonicalizes each entry.
    def canonicalize_environments(manifest)
      canonicalize_key(manifest, :environments) do |environments|
        Hash[
          environments.map do |key, environment|
            [key, canonicalize(environment)]
          end
        ]
      end
    end

    # Takes a hash, a key in that hash, and a block. If the key is present, runs
    # the corresponding value through the block and returns a new hash with the
    # canonicalized value. (This can be merged into the original hash using
    # .update.) Otherwise, returns an empty hash.
    def canonicalize_key(manifest, key)
      if manifest.has_key? key
        val = manifest[key]
        {
          key => yield(val),
        }
      else
        {}
      end
    end

    # Takes a (possibly nested) hash whose keys respond to to_sym
    # Returns a hash where all levels of hashes have only symbol keys
    #
    # {"a" => 1, "b" => { "c" => 2 }}
    #  => {:a => 1, :b => { :c => 2 }}
    #
    # This special-cases the immediate children of any key named 'env' or :env,
    # so that env var names remain (or become) strings.
    #
    # {'env' => { 'a' => { 'b' => 2 }}}
    # => {:env => { 'a' => { :b => 2 }}}
    def keys_to_sym(hash)
      Hash[
        hash.map do |key, val|
          [
            key.to_sym,
            if ['env', :env].include? key
              env_to_sym(val)
            else
              val_to_sym(val)
            end
          ]
        end
      ]
    end

    def keys_to_string(hash)
      Hash[
        hash.map do |key, val|
          [key.to_s, val_to_sym(val)]
        end
      ]
    end

    def val_to_sym(value)
      if value.is_a? Hash
        keys_to_sym(value)
      else
        value
      end
    end

    def env_to_sym(value)
      if value.is_a? Hash
        keys_to_string(value)
      else
        value
      end
    end
  end
end