require "lib/knife/knife_factory"
require "lib/puts_and_flush"
require "hooks"
require_relative "server_executor/expiration_scheduler"
require_relative 'server_executor/server_operation_result'
require "lib/ssh/ssh_utils"
require "exceptions/bootstrap_error"
require "exceptions/deploy_error"
require "exceptions/deploy_info_error"
require "exceptions/creation_error"
require "db/mongo/models/image"
require "db/mongo/models/server"
require "db/mongo/models/key"
require "db/mongo/models/project"

module Devops
  module Executor
    class ServerExecutor
      include Hooks
      include PutsAndFlush
      include ServerOperationResultCode

      # waiting for 5*60 seconds (5 min)
      MAX_SSH_RETRIES_AMOUNT = 60

      define_hook :before_create
      define_hook :after_create

      #params:
      #  out - container for output data
      define_hook :before_bootstrap
      define_hook :after_bootstrap

      #before_deploy :add_run_list_to_deploy_info

      attr_accessor :server, :environment, :job_task, :project
      attr_reader :out, :current_user

      def initialize server, out, current_user, options={}
        if server
          @project = Devops::Model::Project.find_with_environment(server.project, server.environment)
          @environment = @project.environment(server.environment)
          @category = @environment.get_category(server.category)
          @server = server
        end
        @out = out
        @out.class.send(:define_method, :flush) { } unless @out.respond_to?(:flush)
        @current_user = current_user
      end

      # TODO: refactore attr_accessors to constructor options,
      # because these values don't change after object initializing
      def job_task= task
        @job_task = task
      end

      def project= p
        @project = p
      end

      def environment= e
        @environment = e
      end

      def server
        @server
      end

      def category= category
        @category = category
      end

      def create_server_object options
        Devops::Model::Server.new({
          "project" => @project.id,
          "environment" => @environment.id,
          "category" => @category.id,
          "created_by" => @current_user,
          "provider" => @category.provider.name,
          "provider_account" => @category.provider.account,
          "private_ip" => options["private_ip"]
        })
      end

      def create_server options
        @server = create_server_object(options)
        @server.set_last_operation(Devops::Model::Server::OperationType::CREATION, @current_user, @job_task.id)
        provider = @category.provider
        @out.puts "Create server..."
        @server.run_list = options["run_list"] || []
        @server.ssh_key = options["ssh_key"] || provider.account_instance.ssh_key
        @server.name = options["name"] || @server.provider_instance.create_default_server_name(@server)

        image = Devops::Model::Image.where(image_id: provider.image, provider: provider.name, provider_account: provider.account).first
        @server.remote_user = image.remote_user

        res = {}
        @out << "\nBefore create hooks...\n"
        res[:before] = self.run_hook :before_create
        @out << "Done\n"

        @out.puts "Running cloud instance...\nUsing provider '#{provider.name}' and account '#{provider.account}'\n"
        @out.flush

        unless provider.create_server(@server, @out)
          raise Devops::Exception::CreationError.new("Can not create server in cloud with provider '#{provider.name}' and account '#{provider.account}'")
        end
        @server.save

        provider.waiting_server @server, @out
        @server.save

        @out << @server.info
        @out.flush

        @out << "\n\nAfter create hooks...\n"
        res[:after] = self.run_hook :after_create
        @out << "Done\n"
        @out.flush
        DevopsLogger.logger.info "Server with parameters: #{@server.to_hash.inspect} is running"

        schedule_expiration()
        0
      end

      def deploy_info options
        @server.set_last_operation(Devops::Model::Server::OperationType::DEPLOY_INFO, @current_user, @job_task.id)
        options[:deploy_info] || @project.deploy_info(@environment)
      end

      def bootstrap_server options, deploy_info
        @server.set_last_operation(Devops::Model::Server::OperationType::BOOTSTRAP, @current_user, @job_task.id)
=begin
<<<<<<< HEAD
            if @project.is_sandbox?
              bootstrap_options[:deployers] = [options['created_by']]
              bootstrap_options[:deployers] += (options['project_info']['deployers'] || []) if options['project_info']
            end
=======
=end

        @job_task.bootstrap_info = @category.cm_tool.bootstrap_instance(@server, deploy_info, @out, options)
        @job_task.save
        @server.save
        DevopsLogger.logger.info "Server #{@server.id} has been bootstraped with cm name '#{@server.cm_name}'"
        0
      end

      def deploy_server options, deploy_info
        @server.set_last_operation(Devops::Model::Server::OperationType::DEPLOY, @current_user, @job_task.id)
        deploy_info[:run_list] = compute_run_list
        @job_task.deploy_info = deploy_info
        @job_task.save
        @category.cm_tool.deploy_instance(@server, deploy_info, @out)
        DevopsLogger.logger.info "Server #{@server.id} has been deployed"
        0
      end

      def unbootstrap_server
        res = @category.cm_tool.delete_instance(@server, @out)
        res
      end

      def deploy_server_with_tags tags, deploy_info
        unless tags.empty?
          deploy_info[tags: tags]
        end

        @job_task.deploy_info = deploy_info
        @job_task.save
        r = @category.cm_tool.deploy_instance(@server, deploy_info, @out)

        @server.set_last_operation(Devops::Model::Server::OperationType::DEPLOY, @current_user, @job_task.id)
        @server.save
        return r
      end

      def delete_server
        if @server.static?
          @out << "'#{@server.id}' is a static server"
          @out.flush
          unless @server.cm_name.nil?
            unbootstrap_server
          end
          @server.delete
          msg = "Static server '#{@server.id}' is removed"
          DevopsLogger.logger.info msg
          return msg, nil
        end
        puts_and_flush "'#{@server.id}' is not a static server"
        puts_and_flush @server.to_hash
        @category.cm_tool.delete_instance(@server, @out)
        provider = @server.provider_instance
        msg = nil
        begin
          msg = provider.delete_server @server
        rescue Fog::Compute::OpenStack::NotFound, Fog::Compute::AWS::NotFound
          msg = "Server with id '#{@server.id}' not found in '#{provider.name}' servers"
          DevopsLogger.logger.warn msg
        end
        @out.puts msg
        @server.delete
        info = "Server '#{@server.id}' with name '#{@server.name}' for project '#{@server.project}-#{@server.environment}' is removed"
        puts_and_flush info
        DevopsLogger.logger.info info
        return 0
      end

      def roll_back
        @out.puts "Trying to roll back..."
        unless @server.id.nil?
          unless @server.name.nil?
            @out.puts "Server '#{@server.name}' with id '#{@server.id}' is not created"
            @category.cm_tool.delete_instance(@server, @out)
          end
          begin
            @out.puts @server.provider_instance.delete_server(@server)
          rescue => e
            @out.puts e.message
          end
          @out << "\nRolled back\n"
        end
      end

      def add_run_list_to_deploy_info out, deploy_info
        out << "\nGenerate run list hook...\n"
        if deploy_info["run_list"]
          out << "Deploy info already contains 'run_list': #{deploy_info["run_list"].join(", ")}\n"
          return
        end
        out << "Project run list: #{@project.run_list.join(", ")}\n"
        out << "Deploy environment run list: #{@environment.run_list.join(", ")}\n"
        out << "Server run list: #{@server.run_list.join(", ")}\n"
        deploy_info["run_list"] = compute_run_list
        out << "New deploy run list: #{deploy_info["run_list"].join(", ")}\nRun list has been generated\n\n"
      end

      def compute_run_list
        rlist = []
        [@server.provider_instance.run_list, @project.run_list, @environment.run_list, @server.run_list].each do |sub_run_list|
          rlist += sub_run_list if sub_run_list.is_a?(Array)
        end
        if @server.stack
          #TODO
          stack = Devops::Db.connector.stack(@server.stack)
          srl = stack.run_list
          rlist += srl if srl
        end
        rlist.uniq
      end

      def waiting_ssh
        ip = @server.private_ip
        unless @server.public_ip.nil?
          ip = server.public_ip
          @out.puts "\nPublic IP is present"
        end

        @out << "Waiting for SSH"
        k = Devops::Model::Key.find(@server.ssh_key)

        retries_amount = 0
        begin
          sleep(5)
          res = Devops::SSH::Utils.try_ssh(ip, @server.remote_user, k.path)
          if res
            @out.puts "\n"
            @out.flush
            return true
          else
            retries_amount += 1
            if retries_amount > MAX_SSH_RETRIES_AMOUNT
              raise Devops::Exception::BootstrapError.new("Can not connect to #{@server.remote_user}@#{ip}")
            end
            raise ArgumentError.new
          end
        rescue ArgumentError => e
          @out << "."
          @out.flush
          retry
        end
      end

      private
      def schedule_expiration
        if @environment.expires
          job_id = ExpirationScheduler.new(@environment.expires, @server).schedule_expiration!
          puts_and_flush "Planning expiration in #{@environment.expires}, job_id: #{job_id}"
        end
      end

    end
  end
end
