module EY
  module Model
    class Environment < ApiStruct.new(:id, :account, :name, :framework_env, :instances, :instances_count,
                                      :apps, :app_master, :username, :app_server_stack_name, :deployment_configurations,
                                      :load_balancer_ip_address, :api)
      require 'launchy'

      attr_accessor :ignore_bad_master

      def self.from_hash(hash)
        super.tap do |env|
          env.username = hash['ssh_username']
          env.apps = App.from_array(env.apps, :api => env.api)
          env.account = Account.from_hash(env.account)
          env.instances = Instance.from_array(hash['instances'], :environment => env)
          env.app_master = Instance.from_hash(env.app_master.merge(:environment => env)) if env.app_master
        end
      end

      def self.from_array(*)
        Collection::Environments.new(super)
      end

      def logs
        Log.from_array(api_get("/environments/#{id}/logs")["logs"])
      end

      def app_master!
        master = app_master
        if master.nil?
          raise NoAppMasterError.new(name)
        elsif !ignore_bad_master && master.status != "running"
          raise BadAppMasterStatusError.new(master.status)
        end
        master
      end

      def deploy(app, ref, deploy_options={})
        app_master!.deploy(app,
          ref,
          migration_command(app, deploy_options),
          config.merge(deploy_options['extras']),
          deploy_options['verbose'])
      end

      def rollback(app, extra_deploy_hook_options={}, verbose=false)
        app_master!.rollback(app,
          config.merge(extra_deploy_hook_options),
          verbose)
      end

      def take_down_maintenance_page(app, verbose=false)
        app_master!.take_down_maintenance_page(app, verbose)
      end

      def put_up_maintenance_page(app, verbose=false)
        app_master!.put_up_maintenance_page(app, verbose)
      end

      def rebuild
        api.request("/environments/#{id}/update_instances", :method => :put)
      end

      def run_custom_recipes
        api.request("/environments/#{id}/run_custom_recipes", :method => :put)
      end

      def download_recipes
        if File.exist?('cookbooks')
          raise EY::Error, "Could not download, cookbooks already exists"
        end

        require 'tempfile'
        tmp = Tempfile.new("recipes")
        tmp.write(api.request("/environments/#{id}/recipes"))
        tmp.flush
        tmp.close

        cmd = "tar xzf '#{tmp.path}' cookbooks"

        unless system(cmd)
          raise EY::Error, "Could not unarchive recipes.\nCommand `#{cmd}` exited with an error."
        end
      end

      def upload_recipes_at_path(recipes_path)
        recipes_path = Pathname.new(recipes_path)
        if recipes_path.exist?
          upload_recipes recipes_path.open('rb')
        else
          raise EY::Error, "Recipes file not found: #{recipes_path}"
        end
      end

      def tar_and_upload_recipes_in_cookbooks_dir
        require 'tempfile'
        unless File.exist?("cookbooks")
          raise EY::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
        end

        recipes_file = Tempfile.new("recipes")
        cmd = "tar czf '#{recipes_file.path}' cookbooks/"

        unless system(cmd)
          raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
        end

        upload_recipes(recipes_file)
      end

      def upload_recipes(file_to_upload)
        api.request("/environments/#{id}/recipes", {
          :method => :post,
          :params => {:file => file_to_upload}
        })
      end

      # If force_ref is a string, use it as the ref, otherwise use it as a boolean.
      def resolve_branch(ref, force_ref=false)
        if String === force_ref
          ref, force_ref = force_ref, true
        end

        if !ref
          default_branch
        elsif force_ref || !default_branch || ref == default_branch
          ref
        else
          raise BranchMismatchError.new(default_branch, ref)
        end
      end

      def configuration
        EY.config.environments[self.name] || {}
      end
      alias_method :config, :configuration

      def default_branch
        EY.config.default_branch(name)
      end

      def shorten_name_for(app)
        name.gsub(/^#{Regexp.quote(app.name)}_/, '')
      end

      def launch
        Launchy.open(app_master!.hostname_url)
      end

      def migration_command(app, deploy_options)
        # regarding deploy_options['migrate']:
        #
        # missing means migrate how the yaml file says to
        # nil means don't migrate
        # true means migrate w/custom command (if present) or default
        # a string means migrate with this specific command
        return nil if no_migrate?(deploy_options)
        command = migration_command_from_command_line(deploy_options)
        unless command
          return nil if no_migrate?(config)
          command = migration_command_from_config
        end
        command = migration_command_from_environment(app) unless command
        command
      end

      private

      def no_migrate?(hash)
        hash.key?('migrate') && hash['migrate'] == false
      end

      def migration_command_from_config
        config['migration_command'] if config['migrate'] || config['migration_command']
      end

      def migration_command_from_command_line(deploy_options)
        if migrate = deploy_options['migrate']
          migrate.respond_to?(:to_str) ? migrate.to_str : (config['migration_command'] || default_migration_command)
        end
      end

      def migration_command_from_environment(app)
        if deploy_config = deployment_configurations[app.name]
          deploy_config['migrate']['command'] if deploy_config['migrate']['perform']
        end
      end

      def default_migration_command
        'rake db:migrate'
      end
    end
  end
end
