#-- encoding: UTF-8

#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++

require 'model_contract'

module WorkPackages
  class BaseContract < ::ModelContract
    include ::Attachments::ValidateReplacements

    def self.model
      WorkPackage
    end

    attribute :subject
    attribute :description
    attribute :status_id
    attribute :type_id
    attribute :priority_id
    attribute :category_id
    attribute :fixed_version_id do
      validate_fixed_version_is_assignable
    end

    validate :validate_no_reopen_on_closed_version

    attribute :lock_version
    attribute :project_id

    attribute :done_ratio,
              writeable: ->(*) {
                model.leaf? && Setting.work_package_done_ratio == 'field'
              }

    attribute :estimated_hours,
              writeable: ->(*) {
                model.leaf?
              }

    attribute :parent_id do
      validate_user_allowed_to_set_parent if model.changed.include?('parent_id')
    end

    attribute :assigned_to_id do
      next unless model.project

      validate_people_visible :assigned_to,
                              'assigned_to_id',
                              model.project.possible_assignee_members
    end

    attribute :responsible_id do
      next unless model.project

      validate_people_visible :responsible,
                              'responsible_id',
                              model.project.possible_responsible_members
    end

    attribute :start_date,
              writeable: ->(*) {
                model.leaf?
              } do
      if start_before_soonest_start?
        message = I18n.t('activerecord.errors.models.work_package.attributes.start_date.violates_relationships',
                         soonest_start: model.soonest_start)

        errors.add :start_date, message, error_symbol: :violates_relationships
      end
    end

    attribute :due_date,
              writeable: ->(*) {
                model.leaf?
              }

    validates :due_date,
              date: { after_or_equal_to: :start_date,
                      message: :greater_than_or_equal_to_start_date,
                      allow_blank: true },
              unless: Proc.new { |wp| wp.start_date.blank? }
    validate :validate_enabled_type

    validate :validate_milestone_constraint
    validate :validate_parent_not_milestone

    validate :validate_parent_exists
    validate :validate_parent_in_same_project
    validate :validate_parent_not_subtask

    validate :validate_status_transition

    validate :validate_active_priority

    validate :validate_category
    validate :validate_estimated_hours

    def initialize(work_package, user)
      super(work_package, user)

      @can = WorkPackagePolicy.new(user)
    end

    def writable_attributes
      # If we're in a readonly status and did not move into that status right now
      # only allow other status transitions
      if model.readonly_status? && !model.status_id_change
        return %w[status status_id]
      end

      super + model.available_custom_fields.map { |cf| "custom_field_#{cf.id}" }
    end

    private

    attr_reader :can

    def validate_estimated_hours
      if !model.estimated_hours.nil? && model.estimated_hours < 0
        errors.add :estimated_hours, :only_values_greater_or_equal_zeroes_allowed
      end
    end

    def validate_enabled_type
      # Checks that the issue can not be added/moved to a disabled type
      if model.project && (model.type_id_changed? || model.project_id_changed?)
        errors.add :type_id, :inclusion unless model.project.types.include?(model.type)
      end
    end

    def validate_milestone_constraint
      if model.is_milestone? && model.due_date && model.start_date && model.start_date != model.due_date
        errors.add :due_date, :not_start_date
      end
    end

    def validate_parent_not_milestone
      if model.parent&.is_milestone?
        errors.add :parent, :cannot_be_milestone
      end
    end

    def validate_parent_exists
      if model.parent&.is_a?(WorkPackage::InexistentWorkPackage)

        errors.add :parent, :does_not_exist
      end
    end

    def validate_parent_in_same_project
      if parent_in_different_project?
        errors.add :parent, :cannot_be_in_another_project
      end
    end

    # have to validate ourself as the parent relation is created after saving
    def validate_parent_not_subtask
      if model.parent_id_changed? && model.parent && invalid_relations_with_new_hierarchy.exists?
        errors.add :base, :cant_link_a_work_package_with_a_descendant
      end
    end

    def validate_status_transition
      if status_changed? && status_exists? && !(model.type_id_changed? || status_transition_exists?)
        errors.add :status_id, :status_transition_invalid
      end
    end

    def validate_active_priority
      if model.priority && !model.priority.active? && model.priority_id_changed?
        errors.add :priority_id, :only_active_priorities_allowed
      end
    end

    def validate_category
      if inexistent_category?
        errors.add :category, :does_not_exist
      elsif category_not_of_project?
        errors.add :category, :only_same_project_categories_allowed
      end
    end

    def validate_fixed_version_is_assignable
      if model.fixed_version_id && !model.assignable_versions.map(&:id).include?(model.fixed_version_id)
        errors.add :fixed_version_id, :inclusion
      end
    end

    def validate_user_allowed_to_set_parent
      errors.add :base, :error_unauthorized unless @can.allowed?(model, :manage_subtasks)
    end

    def validate_no_reopen_on_closed_version
      if model.fixed_version_id && model.reopened? && model.fixed_version.closed?
        errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)
      end
    end

    def validate_people_visible(attribute, id_attribute, list)
      id = model[id_attribute]

      return if id.nil? || !model.changed.include?(id_attribute)

      unless principal_visible?(id, list)
        errors.add attribute,
                   I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
                          property: I18n.t("attributes.#{attribute}"))
      end
    end

    def principal_visible?(id, list)
      list.exists?(user_id: id)
    end

    def start_before_soonest_start?
      model.start_date &&
        model.soonest_start &&
        model.start_date < model.soonest_start
    end

    def parent_in_different_project?
      model.parent &&
        model.parent.project != model.project &&
        !Setting.cross_project_work_package_relations? &&
        !model.parent.is_a?(WorkPackage::InexistentWorkPackage)
    end

    def inexistent_category?
      model.category_id.present? && !model.category
    end

    def category_not_of_project?
      model.category && !model.project.categories.include?(model.category)
    end

    def status_changed?
      model.status_id_was != 0 && model.status_id_changed?
    end

    def status_exists?
      model.status_id && model.status
    end

    def status_transition_exists?
      model.type.valid_transition?(model.status_id_was,
                                   model.status_id,
                                   user.roles(model.project))
    end

    def invalid_relations_with_new_hierarchy
      query = Relation.from_parent_to_self_and_descendants(model)
                      .or(Relation.from_self_and_descendants_to_ancestors(model))
                      .direct

      # Ignore the immediate relation from the old parent to the model
      # since that will still exist before saving.
      old_parent_id = model.parent_id_was

      if old_parent_id.present?
        query.where.not(hierarchy: 1, from_id: old_parent_id, to_id: model.id)
      else
        query
      end
    end
  end
end
