# frozen_string_literal: true

# Redmine plugin for Custom Workflows
#
# Anton Argirov, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine OAuth plugin.
#
# Redmine Custom Workflows plugin is free software: you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
#  option) any later version.
#
# Redmine Custom Workflows plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along with Redmine Custom Workflows plugin. If not,
# see <https://www.gnu.org/licenses/>.

# Custom workflow model
class CustomWorkflow < ApplicationRecord
  OBSERVABLES = %i[issue issue_relation issue_attachments user member attachment group group_users project
                   project_attachments wiki_content wiki_page_attachments time_entry version shared].freeze
  PROJECT_OBSERVABLES = %i[issue issue_attachments project project_attachments wiki_content wiki_page_attachments
                           time_entry version member].freeze
  COLLECTION_OBSERVABLES = %i[group_users issue_attachments project_attachments wiki_page_attachments].freeze
  SINGLE_OBSERVABLES = %i[issue issue_relation user member group attachment project wiki_content time_entry version]
                       .freeze

  acts_as_list

  has_and_belongs_to_many :projects

  validates :name, uniqueness: true
  validates :author, format: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, allow_blank: true
  validate :validate_syntax, :validate_scripts_presence,
           if: proc { |workflow| workflow.respond_to?(:observable) and workflow.active? }

  scope :active, -> { where(active: true) }
  scope :sorted, -> { order(:position) }
  scope :for_project, (lambda do |project|
    where(
      %{
        is_for_all = ? OR
        EXISTS (SELECT * FROM #{reflect_on_association(:projects).join_table}
        WHERE project_id = ? AND custom_workflow_id = id)
      },
      true,
      project.id
    )
  end)

  def self.import_from_xml(xml)
    attributes = Hash.from_xml(xml).values.first
    attributes.delete 'id'
    attributes.delete 'exported_at'
    attributes.delete 'plugin_version'
    attributes.delete 'ruby_version'
    attributes.delete 'rails_version'
    name = attributes['name']
    i = 1
    while CustomWorkflow.exists?(name: name)
      name = "#{attributes['name']}_#{i}"
      i += 1
    end
    attributes['name'] = name
    CustomWorkflow.new attributes
  end

  def self.log_message(str, object)
    Rails.logger.info "#{str} for #{object.class} (##{object.id}) \"#{object}\""
  end

  def self.run_shared_code?(object)
    # Due to DB migration
    if CustomWorkflow.table_exists? && CustomWorkflow.active.exists?(observable: :shared)
      log_message '= Running shared code', object
      CustomWorkflow.active.where(observable: :shared).sorted.each do |workflow|
        unless workflow.run(object, :shared_code)
          log_message '= Abort running shared code', object
          return false
        end
      end
      log_message '= Finished running shared code', object
    end
    true
  end

  def self.run_custom_workflows?(observable, object, event)
    if CustomWorkflow.table_exists? # Due to DB migration
      workflows = CustomWorkflow.active.where(observable: observable)
      if PROJECT_OBSERVABLES.include? observable
        return true unless object.project

        workflows = workflows.for_project(object.project)
      end
      return true unless workflows.any?

      log_message "= Running #{event} custom workflows", object
      workflows.sorted.each do |workflow|
        unless workflow.run(object, event)
          log_message "= Abort running #{event} custom workflows", object
          return false
        end
      end
      log_message "= Finished running #{event} custom workflows", object
    end
    true
  end

  def run(object, event)
    return true unless attribute_present?(event)

    Rails.logger.info "== Running #{event} custom workflow \"#{name}\""
    object.instance_eval self[event]
    true
  rescue RedmineCustomWorkflows::Errors::WorkflowError => e
    Rails.logger.info "== User workflow error: #{e.message}"
    object.errors.add :base, e.message
    false
  rescue StandardError => e
    Rails.logger.error "== Custom workflow #{name}, ##{id} exception: #{e.message}\n #{e.backtrace.join("\n ")}"
    object.errors.add :base, :custom_workflow_error
    false
  end

  def projects_association?
    PROJECT_OBSERVABLES.include? observable.to_sym
  end

  def validate_syntax_for(object, event)
    object.instance_eval(self[event]) if respond_to?(event) && self[event]
  rescue RedmineCustomWorkflows::Errors::WorkflowError => _e
    # Do nothing
  rescue StandardError, ScriptError => e
    errors.add event, :invalid_script, error: e
  end

  def validate_scripts_presence
    fields = case observable.to_sym
             when :shared
               [shared_code]
             when *SINGLE_OBSERVABLES
               [before_save, after_save, before_destroy, after_destroy]
             when *COLLECTION_OBSERVABLES
               [before_add, after_add, before_remove, after_remove]
             else
               []
             end
    errors.add(:base, :scripts_absent) unless fields.any?(&:present?)
  end

  def validate_syntax
    case observable.to_sym
    when :shared
      CustomWorkflow.run_shared_code? self
      validate_syntax_for self, :shared_code
    when *SINGLE_OBSERVABLES
      object = observable.camelize.constantize.new
      object.send :instance_variable_set, "@#{observable}", object # compatibility with 0.0.1
      CustomWorkflow.run_shared_code? object
      %i[before_save after_save before_destroy after_destroy].each { |field| validate_syntax_for object, field }
    when *COLLECTION_OBSERVABLES
      object = nil
      case observable.to_sym
      when :group_users
        object = Group.new
        object.send :instance_variable_set, :@user, User.new
        object.send :instance_variable_set, :@group, object
      when :issue_attachments
        object = Issue.new
        object.send :instance_variable_set, :@attachment, Attachment.new
        object.send :instance_variable_set, :@issue, object
      when :project_attachments
        object = Project.new
        object.send :instance_variable_set, :@attachment, Attachment.new
        object.send :instance_variable_set, :@project, object
      when :wiki_page_attachments
        object = WikiPage.new
        object.send :instance_variable_set, :@attachment, Attachment.new
        object.send :instance_variable_set, :@page, object
      end
      CustomWorkflow.run_shared_code? object
      %i[before_add after_add before_remove after_remove].each { |field| validate_syntax_for object, field }
    end
  end

  def export_as_xml
    attrs = attributes.dup
    attrs['exported-at'] = Time.current.xmlschema
    attrs['plugin-version'] = Redmine::Plugin.find(:redmine_custom_workflows).version
    attrs['ruby-version'] = "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
    attrs['rails-version'] = Rails::VERSION::STRING
    attrs.to_xml
  end

  def <=>(other)
    position <=> other.position
  end

  def to_s
    name
  end
end
