require "uri"

require "commands/status"
require "commands/bootstrap_templates"
require 'exceptions/conflict_exception'
require "lib/executors/server_executor"

require "db/mongo/models/server"

require "workers/create_server_worker"
require "workers/delete_server_worker"
require "workers/bootstrap_worker"
require "workers/unbootstrap_worker"
require "app/api3/parsers/server"
require_relative "request_handler"

module Devops
  module API3
    module Handler
      class Server < RequestHandler

        set_parser Devops::API3::Parser::ServerParser

        extend StatusCommands
        extend BootstrapTemplatesCommands

        def servers
          Devops::Model::Server.where(parser.servers).all
        end

        def provider_servers provider
          provider_servers_with_account provider, nil
        end

        def provider_servers_with_account provider, account
          ::Provider.get_connector(provider, account).servers
        end

        def server id
          Devops::Model::Server.find(id)
        rescue Mongoid::Errors::DocumentNotFound
          raise Devops::Exception::RecordNotFound.new("Server '#{id}' not found")
        end

        # ids - array of servers ids
        # returns array of jid
        def delete ids
          user = parser.current_user
          jids = []
          ids.each do |id|
            DevopsLogger.logger.debug("Trying to delete server '#{id}'")
            begin
              s = server(id)
              Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
              jid = Worker.start_async(DeleteServerWorker,
                server_id: s.id,
                user: user
              )
              jids << jid
            rescue Devops::Exception::RecordNotFound, Devops::Exception::Unauthorized => e
              DevopsLogger.logger.warn(e.message)
            end
          end
          jids
        end

        def server_id_by_name name
          s = Devops::Model::Server.find_by(name: name)
          s.id
        rescue Mongoid::Errors::DocumentNotFound
          raise Devops::Exception::RecordNotFound.new("Server with name '#{name}' not found")
        end

        # ids - array of servers ids
        def delete_stream ids, out
          user = parser.current_user
          ids.each do |id|
            begin
              s = server(id)
              ### Authorization
              Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
              Devops::Executor::ServerExecutor.new(s, out).delete_server
            rescue Devops::Exception::RecordNotFound
              out.puts "Server '#{id}' not found"
            rescue Devops::Exception::Unauthorized
              out.puts "User '#{user}' can not delete server '#{id}'"
            end
          end
        end

        def create_server_stream out
          status = []
          prepare_create_server do |project, env, user, body|
            e = Devops::Executor::ServerExecutor.new(nil, out)
            e.project = project
            e.environment = env
            body["created_by"] = user
            res = e.create_server(body)
            status.push res
          end
          status
        end

        def create_server
          body = parser.create
          user = parser.current_user

          check_if_server_attrs_are_valid(body, user)
          jid = Worker.start_async(CreateServerWorker,
            server_attrs: body,
            owner: user
          )
          [jid]
        end

        def deploy id
          call_deploy(id) do |options|
            [ Worker.start_async(DeployWorker, options) ]
          end
        end

        def call_stream out
          yield
        rescue Devops::Exception::RecordNotFound => e
          out << e.message
          NOT_FOUND
        rescue Devops::Exception::ConflictError => e
          out << e.message
          CONFLICT
        rescue Devops::Exception::Unauthorized => e
          out << e.message
          UNAUTHORIZED
        end

        def deploy_stream id, out
          call_stream(out) do
            call_deploy(id) do |options|
              Worker.start_sync(DeployWorker, options, out)
            end
          end
        end

        def call_deploy id
          body = parser.deploy
          tags = body["tags"] || []
          run_list = body["run_list"]
          owner = parser.current_user
          s = server(id)
          project = Devops::Model::Project.check_user_authorization(s.project, s.environment, owner)
          deploy_info = create_deploy_info(s, project, body["build_number"])
          deploy_info["run_list"] = run_list if run_list

          yield({
            server_id: id,
            owner: owner,
            tags: tags,
            deploy_info: deploy_info
          })
        end

        def create_deploy_info server, project, build_number
          env_model = project.environment(server.environment)
          project.deploy_info(env_model, build_number)
        end

        def pause_server id
          s = Devops::Model::Server.find(id)
          ## Authorization
          Devops::Model::Project.check_user_authorization(s.project, s.environment, parser.current_user)
          provider = s.provider_instance
          r = provider.pause_server s
          if r.nil?
            set_last_operation_and_save(s, Devops::Model::Server::OperationType::PAUSE)
            "Server with instance ID '#{s.id}' has been paused"
          else
            raise Devops::Exception::ConflictError.new("Server with instance ID '#{s.id}' can not be paused, It in state '#{r}'")
          end
        end

        def unpause_server id
          s = Devops::Model::Server.find(id)
           ## Authorization
          Devops::Model::Project.check_user_authorization(s.project, s.environment, parser.current_user)
          provider = s.provider_instance
          r = provider.unpause_server s
          if r.nil?
            set_last_operation_and_save(s, Devops::Model::Server::OperationType::UNPAUSE)
            "Server with instance ID '#{s.id}' has been unpaused"
          else
            raise Devops::Exception::ConflictError.new("Server with instance ID '#{s.id}' can not be unpaused, It in state '#{r}'")
          end
        end

        def reserve_server id
          s = Devops::Model::Server.find(id)
          user = parser.current_user
          Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
          raise Devops::Exception::ConflictError.new("Server '#{id}' already reserved") unless s.reserved_by.nil?
          s.reserved_by = user
          set_last_operation_and_save(s, Devops::Model::Server::OperationType::RESERVE)
        end

        def unreserve_server id
          s = Devops::Model::Server.find(id)
          user = parser.current_user
          Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
          raise Devops::Exception::ConflictError.new("Server '#{id}' is not reserved") if s.reserved_by.nil?
          s.reserved_by = nil
          set_last_operation_and_save(s, Devops::Model::Server::OperationType::UNRESERVE)
        end

        # TODO: check bootstrap template name
        def bootstrap_server_stream id, out
          call_stream(out) do
            call_bootstrap(id) do |options|
              Worker.start_sync(BootstrapWorker, options, out)
            end
          end
        end

        def bootstrap_server(id)
          call_bootstrap(id) do |options|
            Worker.start_async(BootstrapWorker, options)
          end
        end

        def call_bootstrap id
          body = parser.bootstrap
          name = body["cm_name"]
          rl = body["run_list"]
          t = body["bootstrap_template"]
          s = server(id)

          user = parser.current_user
          p = Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
          raise Devops::Exception::ConflictError.new("Server '#{id}' already bootstrapped") unless s.cm_name.nil?
          attrs = {
            server_id: id,
            owner: user
          }

          d = p.environment(s.environment)

          provider = s.provider_instance

          if name
            d.get_category(s.category).cm_tool.check_node_name(name)
            attrs[:cm_name] = name
          end
          unless t.nil?
            templates = get_templates
            raise ValidationError.new("Invalid bootstrap template '#{t}', available values: #{templates.join(", ")}") unless templates.include?(t)
            attrs[:bootstrap_template] = t
          end
          attrs[:run_list] = rl if rl

          yield(attrs)
        end

        def unbootstrap_server id
          call_unbootstrap(id) do |options|
            Worker.start_async(UnbootstrapWorker, options)
          end
        end

        def unbootstrap_server_stream id, out
          call_stream(out) do
            call_unbootstrap(id) do |options|
              Worker.start_sync(UnbootstrapWorker, options)
            end
          end
        end

        def call_unbootstrap id
          s = server(id)
          user = parser.current_user
          ### Authorization
          Devops::Model::Project.check_user_authorization(s.project, s.environment, user)
          raise Devops::Exception::ConflictError.new("Server '#{id}' is not bootstrapped") if s.cm_name.nil?
          attrs = {
            server_id: id,
            owner: parser.current_user
          }
          yield(attrs)
        end

        def add_server
          body = parser.add_server
          user = parser.current_user
          project = Devops::Model::Project.check_user_authorization(body[:project], body[:environment], user)
          env = project.environment(body[:environment])
          category = env.get_category!(body[:category])
          raise 'Provider should be static' if category.provider.name != 'static'
          provider_account = Model::ProviderAccount.find(category.provider.account)
          provider = ::Provider.provider('static')
          Devops::Model::Server.create!(
            "provider" => provider.name,
            "provider_account" => provider_account.id,
            "project" => project.id,
            "environment" => env.id,
            "category" => category.id,
            "remote_user" => body[:remote_user],
            "private_ip" => body[:private_ip],
            "public_ip" => body[:public_ip],
            "id" => "static_#{provider_account.ssh_key}-#{Time.now.to_i}",
            "ssh_key" => provider_account.ssh_key,
            'created_by' => user,
            'cm_name' => nil,
            'name' => body[:name]
          )
        end

        def set_tags id
          tags = parser.tags
          prepare_tags(id) do |provider|
            provider.set_tags id, tags
          end
        end

        def unset_tags id
          tags = parser.tags
          prepare_tags(id) do |provider|
            provider.unset_tags id, tags
          end
        end

        def prepare_tags id
          s = Devops::Model::Server.find(id)
          p = Devops::Model::Project.check_user_authorization(s.project, s.environment, parser.current_user)
          provider = s.provider_instance
          yield provider
        end

        def set_run_list id
          s = Devops::Model::Server.find(id)
          Devops::Model::Project.check_user_authorization(s.project, s.environment, parser.current_user)
          s.set(run_list: parser.run_list)
        end

        private

        def check_if_server_attrs_are_valid(body, user)
          if body["project"].nil? or body["environment"].nil? or body["category"].nil?
            raise Devops::Exception::ValidationError.new("Properties 'project', 'environment' and 'category' are mandatory")
          end

          key_name = body["ssh_key"]
          unless key_name.nil?
            begin
              Devops::Model::Key.find(key_name)
            rescue Mongoid::Errors::DocumentNotFound
              raise Devops::Exception::ValidationError.new("Key with id '#{key_name}' not found")
            end
          end

          project = Devops::Model::Project.check_user_authorization(body["project"], body["environment"], user)
          env = project.environment(body["environment"])
          category = env.get_category(body["category"])
          if body["name"]
            begin
              Devops::Model::Server.find_by(name: body["name"])
              raise Devops::Exception::ValidationError.new("Server with name '#{body["name"]}' already exists" )
            rescue Mongoid::Errors::DocumentNotFound
              category.provider.provider_instance.check_node_name(body["name"])
              category.cm_tool.check_node_name(body["name"])
            end
          end

=begin
          provider = env.provider_instance
          server_name = body["name"]
          check_chef_node_name(server_name, provider, KnifeFactory.instance) unless server_name.nil?
          groups = body["groups"]
          unless groups.nil?
            buf = groups - provider.groups.keys
            halt_response("Invalid security groups '#{buf.join("', '")}' for provider '#{provider.name}'") if buf.empty?
          end
=end
        end

        def set_last_operation_and_save(server, operation_type)
          server.set_last_operation(operation_type, parser.current_user, nil)
          server.save
        end

      end
    end
  end
end

