#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
else
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
end

if File.directory?(GEMS_LOCATION)
    Gem.use_paths(GEMS_LOCATION)
end

$LOAD_PATH << RUBY_LIB_LOCATION
$LOAD_PATH << RUBY_LIB_LOCATION + '/cli'

require 'command_parser'
require 'opennebula/oneflow_client'
require 'English'
require 'cli_helper'
require 'one_helper'

require 'json'

USER_AGENT = 'CLI'

# Base Path representing the resource to be used in the requests
RESOURCE_PATH = '/service_template'

#
# Table
#

TABLE = CLIHelper::ShowTable.new(nil, self) do
    column :ID, 'ID', :size => 10 do |d|
        d['ID']
    end

    column :USER, 'Username', :left, :size => 15 do |d|
        d['UNAME']
    end

    column :GROUP, 'Group', :left, :size => 15 do |d|
        d['GNAME']
    end

    column :NAME, 'Name', :left, :size => 37 do |d|
        d['NAME']
    end

    default :ID, :USER, :GROUP, :NAME
end

# Show the service template information. This method is used in top and
# show commands
# @param [Service::Client] client
# @param [Array] args
# @param [Hash] options
# @return [[Integer, String], Integer] Returns the exit_code and optionally
#   a String to be printed
def show_service_template(client, args, options)
    response = client.get("#{RESOURCE_PATH}/#{args[0]}")

    if CloudClient.is_error?(response)
        [response.code.to_i, response.to_s]
    else
        # [0,response.body]
        if options[:json]
            [0, response.body]
        else
            str = '%-20s: %-20s'
            str_h1 = '%-80s'

            document_hash = JSON.parse(response.body)
            template = document_hash['DOCUMENT']['TEMPLATE']['BODY']

            CLIHelper.print_header(str_h1 %
                "SERVICE TEMPLATE #{document_hash['DOCUMENT']['ID']} "\
                'INFORMATION')

            puts Kernel.format str, 'ID',   document_hash['DOCUMENT']['ID']
            puts Kernel.format str, 'NAME', document_hash['DOCUMENT']['NAME']
            puts Kernel.format str, 'USER', document_hash['DOCUMENT']['UNAME']
            puts Kernel.format str, 'GROUP', document_hash['DOCUMENT']['GNAME']

            puts

            CLIHelper.print_header(str_h1 % 'PERMISSIONS', false)

            %w[OWNER GROUP OTHER].each do |e|
                mask = '---'
                permissions_hash = document_hash['DOCUMENT']['PERMISSIONS']
                mask[0] = 'u' if permissions_hash["#{e}_U"] == '1'
                mask[1] = 'm' if permissions_hash["#{e}_M"] == '1'
                mask[2] = 'a' if permissions_hash["#{e}_A"] == '1'

                puts Kernel.format str, e, mask
            end

            puts

            CLIHelper.print_header(str_h1 % 'TEMPLATE CONTENTS', false)
            puts JSON.pretty_generate(template)

            0
        end
    end
end

# List the services. This method is used in top and list commands
# @param [Service::Client] client
# @param [Hash] options
# @return [[Integer, String], Integer] Returns the exit_code and optionally
#   a String to be printed
def list_service_templates(client, options)
    response = client.get(RESOURCE_PATH)

    if CloudClient.is_error?(response)
        [response.code.to_i, response.to_s]
    else
        # [0,response.body]
        if options[:json]
            [0, response.body]
        else
            array_list = JSON.parse(response.body)
            TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT'])
            0
        end
    end
end

#
# Commands
#

CommandParser::CmdParser.new(ARGV) do
    usage '`oneflow-template` <command> [<args>] [<options>]'
    version OpenNebulaHelper::ONE_VERSION

    set :option, Service::DEFAULT_OPTIONS
    set :option, CommandParser::VERSION
    set :option, CommandParser::HELP

    #
    # Formatters for arguments
    #
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'GROUP')
    end

    set :format, :userid, OpenNebulaHelper.rname_to_id_desc('USER') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'USER')
    end

    set :format, :templateid,
        Service.rname_to_id_desc('SERVICE TEMPLATE') do |arg|
        Service.rname_to_id(arg, 'SERVICE TEMPLATE')
    end

    set :format, :templateid_list,
        Service.list_to_id_desc('SERVICE TEMPLATE') do |arg|
        Service.list_to_id(arg, 'SERVICE TEMPLATE')
    end

    #
    # List
    #

    list_desc = <<-EOT.unindent
        List the available Service Templates
    EOT

    command :list, list_desc, :options => Service::JSON_FORMAT do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        list_service_templates(client, options)
    end

    #
    # Top
    #

    top_desc = <<-EOT.unindent
        List the available Service Templates continuously
    EOT

    command :top, top_desc,
            :options => [Service::JSON_FORMAT,
                         Service::TOP,
                         CLIHelper::DELAY] do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        options[:delay] ? delay = options[:delay] : delay = 3

        begin
            loop do
                CLIHelper.scr_cls
                CLIHelper.scr_move(0, 0)

                rc, message = list_service_templates(client, options)

                if rc != 0
                    raise message
                end

                sleep delay
            end
        rescue StandardError => e
            puts e.message
            -1
        end
    end

    #
    # Create
    #

    create_desc = <<-EOT.unindent
        Create a new Service Template
    EOT

    command :create, create_desc, :file, :options => Service::JSON_FORMAT do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        response = client.post(RESOURCE_PATH, File.read(args[0]))

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            if options[:json]
                [0, response.body]
            else
                template = JSON.parse(response.body)
                puts "ID: #{template['DOCUMENT']['ID']}"
                0
            end
        end
    end

    #
    # Show
    #

    show_desc = <<-EOT.unindent
        Show detailed information of a given Service Template
    EOT

    command :show, show_desc, :templateid, :options => Service::JSON_FORMAT do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        show_service_template(client, args, options)
    end

    #
    # Delete
    #

    delete_desc = <<-EOT.unindent
        Delete a given Service Template
    EOT

    command :delete, delete_desc, [:range, :templateid_list] do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        Service.perform_actions(args[0]) do |template_id|
            client.delete("#{RESOURCE_PATH}/#{template_id}")
        end
    end

    #
    # Instantiate
    #

    instantiate_desc = <<-EOT.unindent
        Instantiate a Service Template
    EOT

    command :instantiate, instantiate_desc, :templateid, [:file, nil],
            :options => [Service::JSON_FORMAT, Service::TOP] do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        params = {}

        if args[1]
            params['merge_template'] = JSON.parse(File.read(args[1]))
        end

        json_str = Service.build_json_action('instantiate', params)

        response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json_str)

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            if options[:json]
                [0, response.body]
            else
                template = JSON.parse(response.body)
                puts "ID: #{template['DOCUMENT']['ID']}"
                0
            end
        end
    end

    chgrp_desc = <<-EOT.unindent
        Changes the service template group
    EOT

    command :chgrp, chgrp_desc, [:range, :templateid_list], :groupid do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['group_id'] = args[1].to_i

            json_action = Service.build_json_action('chgrp', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        end
    end

    chown_desc = <<-EOT.unindent
        Changes the service template owner and group
    EOT

    command :chown, chown_desc, [:range, :templateid_list],
            :userid, [:groupid, nil] do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['owner_id'] = args[1]
            params['group_id'] = args[2] if args[2]

            json_action = Service.build_json_action('chown', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        end
    end

    chmod_desc = <<-EOT.unindent
        Changes the service template permissions
    EOT

    command :chmod, chmod_desc, [:range, :templateid_list], :octet do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        Service.perform_actions(args[0]) do |service_id|
            params = {}
            params['octet'] = args[1]

            json_action = Service.build_json_action('chmod', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        end
    end

    clone_desc = <<-EOT.unindent
        Creates a new Service Template from an existing one
    EOT

    command :clone, clone_desc, :templateid, :name do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        params = {}
        params['name'] = args[1]

        json_action = Service.build_json_action('clone', params)

        response = client.post("#{RESOURCE_PATH}/#{args[0]}/action",
                               json_action)

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            if options[:json]
                [0, response.body]
            else
                template = JSON.parse(response.body)
                puts "ID: #{template['DOCUMENT']['ID']}"
                0
            end
        end
    end

    rename_desc = <<-EOT.unindent
        Renames the Service Template
    EOT

    command :rename, rename_desc, :templateid, :name do
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        params = {}
        params['name'] = args[1]

        json_action = Service.build_json_action('rename', params)

        response = client.post("#{RESOURCE_PATH}/#{args[0]}/action",
                               json_action)

        if CloudClient.is_error?(response)
            [response.code.to_i, response.to_s]
        else
            response.code.to_i
        end
    end

    update_desc = <<-EOT.unindent
        Update the template contents. If a path is not provided the editor will
        be launched to modify the current content.
    EOT

    command :update, update_desc, :templateid, [:file, nil] do
        template_id = args[0]
        client = Service::Client.new(
            :username => options[:username],
            :password => options[:password],
            :url => options[:server],
            :user_agent => USER_AGENT
        )

        if args[1]
            path = args[1]
        else
            require 'tempfile'

            tmp  = Tempfile.new(template_id.to_s)
            path = tmp.path

            response = client.get("#{RESOURCE_PATH}/#{template_id}")

            if CloudClient.is_error?(response)
                exit_with_code response.code.to_i, response.to_s
            else
                document_hash = JSON.parse(response.body)
                template = document_hash['DOCUMENT']['TEMPLATE']['BODY']

                tmp << JSON.pretty_generate(template)
                tmp.flush

                if ENV['EDITOR']
                    editor_path = ENV['EDITOR']
                else
                    editor_path = OpenNebulaHelper::EDITOR_PATH
                end
                system("#{editor_path} #{path}")

                unless $CHILD_STATUS.exitstatus.zero?
                    puts 'Editor not defined'
                    exit(-1)
                end

                tmp.close
            end
        end

        exit_code = 0

        t_str = File.read(path)
        response = client.put("#{RESOURCE_PATH}/#{template_id}", t_str)
        if CloudClient.is_error?(response)
            puts response.to_s
            exit_code = response.code.to_i
        end

        exit_code
    end
end
