# encoding: UTF-8
#
# Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

require_relative '../rest/resource'
require_relative '../mixins/rest_resource'
require_relative '../helpers/global_helpers'

require_relative 'execution'

module GoodData
  class Schedule < Rest::Resource
    attr_reader :dirty, :json

    SCHEDULE_TEMPLATE = {
      :schedule => {
        :type => nil,
        :timezone => nil,
        :params => {},
        :hiddenParams => {},
        # :reschedule => nil
      }
    }

    class << self
      # Looks for schedule
      # @param id [String] URL, ID of schedule or :all
      # @return [GoodData::Schedule|Array<GoodData::Schedule>] List of schedules
      def [](id, opts = { :client => GoodData.connection, :project => GoodData.project })
        c, project = GoodData.get_client_and_project(opts)

        if id == :all
          GoodData::Schedule.all(opts)
        else
          if id =~ %r{\/gdc\/projects\/[a-zA-Z\d]+\/schedules\/?[a-zA-Z\d]*}
            url = id
            tmp = c.get url
            return c.create(GoodData::Schedule, tmp)
          end

          tmp = c.get "/gdc/projects/#{project.pid}/schedules/#{id}"
          c.create(GoodData::Schedule, tmp, project: project)
        end
      end

      # Returns list of all schedules for active project
      # @return [Array<GoodData::Schedule>] List of schedules
      def all(opts = { :client => GoodData.connection, :project => GoodData.project })
        c, project = GoodData.get_client_and_project(opts)

        tmp = c.get "/gdc/projects/#{project.pid}/schedules"
        tmp['schedules']['items'].map { |schedule| c.create(GoodData::Schedule, schedule, project: project) }
      end

      # Creates new schedules from parameters passed
      #
      # @param process_id [String] Process ID
      # @param trigger [String|GoodData::Schedule] Trigger of schedule. Can be cron string or reference to another schedule.
      # @param executable [String] Execution executable
      # @param options [Hash] Optional options
      # @return [GoodData::Schedule] New GoodData::Schedule instance
      def create(process_id, trigger, executable, options = {})
        c, project = GoodData.get_client_and_project(options)

        fail 'Process ID has to be provided' if process_id.blank?

        process = Process[process_id, project: project, client: c]
        is_dataload_process = process.type == :dataload

        if is_dataload_process
          dataload_datasets = options[:dataload_datasets] || options['GDC_DATALOAD_DATASETS']
          dataload_datasets = '[]' unless dataload_datasets

          de_synchronize_all = options[:de_synchronize_all] || options['GDC_DE_SYNCHRONIZE_ALL']
        else
          lcm_component = process.type == :lcm
          add_component = process.add_v2_component?
          executable_missing = !lcm_component && !add_component && executable.blank?
          fail 'Executable has to be provided' if executable_missing
        end

        schedule = c.create(GoodData::Schedule, GoodData::Helpers.stringify_keys(GoodData::Helpers.deep_dup(SCHEDULE_TEMPLATE)), client: c, project: project)

        params = { 'PROCESS_ID' => process_id }
        if is_dataload_process
          params['GDC_DATALOAD_DATASETS'] = dataload_datasets
          params['GDC_DE_SYNCHRONIZE_ALL'] = de_synchronize_all if de_synchronize_all
        else
          params['EXECUTABLE'] = executable
        end

        default_opts = {
          :type => 'MSETL',
          :timezone => 'UTC',
          :state => 'ENABLED',
          :params => params,
          # :reschedule => nil
        }

        schedule.name = options[:name]
        schedule.set_trigger(trigger)
        schedule.trigger_execution_status = options[:trigger_execution_status]
        schedule.params = default_opts[:params].merge(options[:params] || {})
        schedule.hidden_params = options[:hidden_params] || {}
        schedule.timezone = options[:timezone] || default_opts[:timezone]
        schedule.state = options[:state] || default_opts[:state]
        schedule.schedule_type = options[:type] || default_opts[:type]
        schedule.reschedule = options[:reschedule] if options[:reschedule]
        schedule
      end
    end

    # Initializes object from raw json
    #
    # @param json [Object] Raw JSON
    # @return [GoodData::Schedule] New GoodData::Schedule instance
    def initialize(json)
      json = GoodData::Helpers.stringify_keys(json)
      super
      @json = json
      self.params = GoodData::Helpers.decode_params(json['schedule']['params'] || {})
      self.hidden_params = GoodData::Helpers.decode_params(json['schedule']['hiddenParams'] || {})
    end

    def after
      project.schedules(trigger_id) if trigger_id
    end

    def after=(schedule)
      fail 'After trigger has to be a schedule object' unless schedule.is_a?(Schedule)
      json['schedule']['triggerScheduleId'] = schedule.obj_id
      @json['schedule']['cron'] = nil
      @dirty = true
    end

    # Deletes schedule
    def delete
      saved? ? client.delete(uri) : nil
    end

    # Disables the schedule.
    #
    # @return [GoodData::Schedule]
    def disable
      @json['schedule']['state'] = 'DISABLED'
      @dirty = true
      self
    end

    # Disables and saves the schedule.
    #
    # @return [Boolean]
    def disable!
      disable
      save
    end

    # Is schedule disabled?
    #
    # @return [Boolean]
    def disabled?
      state == 'DISABLED'
    end

    # Is schedule enabled?
    #
    # @return [Boolean]
    def enabled?
      !disabled?
    end

    # Enables the schedule
    #
    # @return [GoodData::Schedule]
    def enable
      @json['schedule']['state'] = 'ENABLED'
      @dirty = true
      self
    end

    # Enables and saves the schedule
    #
    # @return [GoodData::Schedule]
    def enable!
      enable
      save
    end

    # Executes schedule
    #
    # @param [Hash] opts execution options.
    # @option opts [Boolean] :wait Wait for execution result
    # @return [Object] Raw Response
    def execute(opts = {})
      return nil unless saved?
      opts = { :wait => true }.merge(opts)
      data = {
        :execution => {}
      }
      res = client.post(execution_url, data)
      execution = client.create(GoodData::Execution, res, client: client, project: project)

      return execution unless opts[:wait]
      execution.wait_for_result(opts)
    end

    # Returns execution URL
    #
    # @return [String] Executions URL
    def execution_url
      saved? ? @json['schedule']['links']['executions'] : nil
    end

    # Returns execution state
    #
    # @return [String] Execution state
    def state
      @json['schedule']['state']
    end

    def state=(a_state)
      @json['schedule']['state'] = a_state
    end

    # Returns execution timezone
    #
    # @return [String] Execution timezone
    def timezone
      @json['schedule']['timezone']
    end

    # Assigns execution timezone
    #
    # @param new_timezone [String] Timezone to be set
    def timezone=(new_timezone)
      @json['schedule']['timezone'] = new_timezone
      @dirty = true
    end

    # Returns execution type
    #
    # @return [String] Execution type
    def type
      @json['schedule']['type']
    end

    # Assigns execution type
    #
    # @param new_type [String] Execution type to be set
    def type=(new_type)
      @json['schedule']['type'] = new_type
      @dirty = true
    end

    # Returns execution cron settings
    #
    # @return [String] Cron settings
    def cron
      @json['schedule']['cron']
    end

    # Assigns execution cron settings
    #
    # @param new_cron [String] Cron settings to be set
    def cron=(new_cron)
      @json['schedule']['cron'] = new_cron
      @json['schedule']['triggerScheduleId'] = nil
      @dirty = true
    end

    # Returns reschedule settings
    #
    # @return [Integer] Reschedule settings
    def reschedule
      @json['schedule']['reschedule']
    end

    # Assigns execution reschedule settings
    #
    # @param new_reschedule [Integer] Reschedule settings to be set
    def reschedule=(new_reschedule)
      @json['schedule']['reschedule'] = new_reschedule
      @dirty = true
    end

    # Returns execution process related to this schedule
    #
    # @return [GoodData::Process] Process ID
    def process
      project.processes(process_id)
    end

    # Returns execution process ID
    #
    # @return [String] Process ID
    def process_id
      @json['schedule']['params']['PROCESS_ID']
    end

    def process_id=(new_project_id)
      @json['schedule']['params']['PROCESS_ID'] = new_project_id
      @dirty = true
    end

    # Returns execution executable
    #
    # @return [String] Executable (graph) name
    def executable
      @json['schedule']['params']['EXECUTABLE']
    end

    # Assigns execution executable
    #
    # @param new_executable [String] Executable to be set.
    def executable=(new_executable)
      @json['schedule']['params']['EXECUTABLE'] = new_executable
      @dirty = true
    end

    # Returns enumerator of executions
    #
    # @return [Array] Raw Executions JSON
    def executions
      return nil unless @json
      url = @json['schedule']['links']['executions']
      Enumerator.new do |y|
        loop do
          res = client.get url
          res['executions']['paging']['next']
          res['executions']['items'].each do |execution|
            y << client.create(Execution, execution, :project => project)
          end
          url = res['executions']['paging']['next']
          break unless url
        end
      end
    end

    # Returns hidden_params as Hash
    #
    # @return [Hash] Hidden Parameters
    def hidden_params
      @json['schedule']['hiddenParams']
    end

    # Updates params by merging the current params with new ones
    #
    # @param params_to_merge [Hash] params
    # @return [GoodData::Schedule] Returns self
    def update_params(params_to_merge)
      params_to_merge.each do |k, v|
        set_parameter(k, v)
      end
      @dirty = true
      self
    end

    # Updates hidden params by merging the current params with new ones
    #
    # @param params_to_merge [Hash] params
    # @return [GoodData::Schedule] Returns self
    def update_hidden_params(params_to_merge)
      params_to_merge.each do |k, v|
        set_hidden_parameter(k, v)
      end
      @dirty = true
      self
    end

    # Assigns hidden parameters
    #
    # @param new_hidden_param [String] Hidden parameters to be set
    def hidden_params=(new_hidden_params = {})
      @json['schedule']['hiddenParams'] = GoodData::Helpers.stringify_values(new_hidden_params)
      @dirty = true
      self
    end

    # Returns params as Hash
    #
    # @return [Hash] Parameters
    def params
      @json['schedule']['params']
    end

    # Assigns execution parameters
    #
    # @param params [String] Params to be set
    def params=(new_params = {})
      default_params = {
        'PROCESS_ID' => process_id,
        'EXECUTABLE' => executable
      }
      @json['schedule']['params'] = default_params.merge(GoodData::Helpers.stringify_values(new_params))
      @dirty = true
      self
    end

    def rewrite_deprecated_params
      params['EXECUTABLE'] = params.delete('GRAPH') if params['GRAPH']
    end

    # Saves object if dirty
    #
    # @return [Boolean] True if saved
    def save
      fail 'A timezone has to be provided' if timezone.blank?
      fail 'Schedule type has to be provided' if schedule_type.blank?
      rewrite_deprecated_params
      if @dirty
        if saved?
          res = client.put(uri, to_update_payload)
          @json = Schedule.new(res).json
        else
          res = client.post "/gdc/projects/#{project.pid}/schedules", to_update_payload
          fail 'Unable to create new schedule' if res.nil?
          new_obj_json = client.get res['schedule']['links']['self']
          @json = Schedule.new(new_obj_json).json
        end
        @dirty = false
      end
      self
    end

    # Updates params at key k with val v
    #
    # @param k [String] key
    # @param v [Object] value
    # @return [GoodData::Schedule] Returns self
    def set_parameter(k, v)
      params[k] = v
      @dirty = true
      self
    end

    # Updates hidden params at key k with val v
    #
    # @param k [String] key
    # @param v [Object] value
    # @return [GoodData::Schedule] Returns self
    def set_hidden_parameter(k, v)
      hidden_params[k] = v
      @dirty = true
      self
    end

    def schedule_type
      json['schedule']['type']
    end

    def schedule_type=(type)
      json['schedule']['type'] = type
      @dirty = true
      self
    end

    def time_based?
      cron != nil
    end

    def to_hash
      {
        name: name,
        type: type,
        state: state,
        params: params,
        hidden_params: hidden_params,
        cron: cron,
        trigger_id: trigger_id,
        trigger_execution_status: trigger_execution_status,
        timezone: timezone,
        uri: uri,
        reschedule: reschedule,
        executable: executable,
        process_id: process_id
      }
    end

    def trigger_id
      json['schedule']['triggerScheduleId']
    end

    def trigger_id=(a_trigger)
      json['schedule']['triggerScheduleId'] = a_trigger
      @dirty = true
      self
    end

    def trigger_execution_status
      json['schedule']['triggerExecutionStatus']
    end

    def trigger_execution_status=(trigger_execution_status)
      json['schedule']['triggerExecutionStatus'] = trigger_execution_status
      @dirty = true
      self # rubocop:disable Lint/Void
    end

    def name
      json['schedule']['name']
    end

    def name=(name)
      json['schedule']['name'] = name
      @dirty = true
      self
    end

    def set_trigger(trigger) # rubocop:disable Style/AccessorMethodName
      if trigger.is_a?(String) && trigger =~ /[a-fA-Z0-9]{24}/
        self.trigger_id = trigger
      elsif trigger.is_a?(GoodData::Schedule)
        self.trigger_id = trigger.obj_id
      else
        self.cron = trigger
      end
    end

    # Returns URL
    #
    # @return [String] Schedule URL
    def uri
      @json['schedule']['links']['self'] if @json && @json['schedule'] && @json['schedule']['links']
    end

    def ==(other)
      other.respond_to?(:uri) && other.uri == uri && other.respond_to?(:to_hash) && other.to_hash == to_hash
    end

    def to_update_payload
      res = {
        'schedule' => {
          'name' => name,
          'type' => type,
          'state' => state,
          'timezone' => timezone,
          'cron' => cron,
          'triggerScheduleId' => trigger_id,
          'params' => GoodData::Helpers.encode_public_params(params),
          'hiddenParams' => GoodData::Helpers.encode_hidden_params(hidden_params)
        }
      }
      res['schedule']['triggerExecutionStatus'] = trigger_execution_status if trigger_execution_status
      res['schedule']['reschedule'] = reschedule if reschedule

      res
    end
  end
end
