require 'models/helpers/process_types'
require 'cloud_controller/errors/invalid_relation'

module VCAP::CloudController
  class Space < Sequel::Model
    class InvalidDeveloperRelation < CloudController::Errors::InvalidRelation; end
    class InvalidAuditorRelation < CloudController::Errors::InvalidRelation; end
    class InvalidSupporterRelation < CloudController::Errors::InvalidRelation; end
    class InvalidManagerRelation < CloudController::Errors::InvalidRelation; end
    class InvalidSpaceQuotaRelation < CloudController::Errors::InvalidRelation; end
    class UnauthorizedAccessToPrivateDomain < RuntimeError; end
    # needed for v2 spaces_controller
    class DBNameUniqueRaceError < Sequel::ValidationFailed; end

    SPACE_NAME_REGEX = /\A[[:alnum:][:punct:][:print:]]+\Z/
    SELECT_NEWEST_PROCESS = lambda { |_, processes|
      newest_processes = {}
      processes.group_by(&:app_guid).each_value do |processes_for_app|
        newest_process = processes_for_app.max_by { |p| [p.created_at, p.id] }
        newest_processes[newest_process.guid] = newest_process
      end
      processes.keep_if { |p| newest_processes[p.guid] }
    }

    plugin :many_through_many

    many_to_one :isolation_segment_model,
                primary_key: :guid,
                key: :isolation_segment_guid

    define_user_group :developers, reciprocal: :spaces, before_add: :validate_developer
    define_user_group :managers, reciprocal: :managed_spaces, before_add: :validate_manager
    define_user_group :auditors, reciprocal: :audited_spaces, before_add: :validate_auditor
    define_user_group :supporters, reciprocal: :supporter_spaces, before_add: :validate_supporter

    many_to_one :organization, before_set: :validate_change_organization

    one_to_many :app_models, primary_key: :guid, key: :space_guid

    one_to_many :processes, class: 'VCAP::CloudController::ProcessModel', dataset: -> { ProcessModel.filter(app: app_models) }

    one_to_many :labels, class: 'VCAP::CloudController::SpaceLabelModel', key: :resource_guid, primary_key: :guid
    one_to_many :annotations, class: 'VCAP::CloudController::SpaceAnnotationModel', key: :resource_guid, primary_key: :guid

    many_through_many :apps, [
      %i[spaces id guid],
      %i[apps space_guid guid]
    ], class: 'VCAP::CloudController::ProcessModel', right_primary_key: :app_guid, conditions: { type: ProcessTypes::WEB },
       after_load: SELECT_NEWEST_PROCESS

    one_to_many :events, primary_key: :guid, key: :space_guid
    one_to_many :service_instances
    one_to_many :managed_service_instances,
                dataset: -> { VCAP::CloudController::ServiceInstance.filter(space_id: id, is_gateway_service: true) }
    many_to_many :service_instances_shared_from_other_spaces,
                 left_key: :target_space_guid,
                 left_primary_key: :guid,
                 right_key: :service_instance_guid,
                 right_primary_key: :guid,
                 join_table: :service_instance_shares,
                 class: 'VCAP::CloudController::ServiceInstance'

    many_to_many :routes_shared_from_other_spaces,
                 left_key: :target_space_guid,
                 left_primary_key: :guid,
                 right_key: :route_guid,
                 right_primary_key: :guid,
                 join_table: :route_shares,
                 class: 'VCAP::CloudController::Route'

    one_to_many :service_brokers
    one_to_many :routes
    one_to_many :tasks,
                dataset: -> { TaskModel.filter(app: app_models) }
    many_to_many :security_groups,
                 dataset: lambda {
                   SecurityGroup.left_join(:security_groups_spaces, security_group_id: :id).
                     where(Sequel.or(security_groups_spaces__space_id: id, security_groups__running_default: true)).distinct(:id)
                 },
                 eager_loader: lambda { |spaces_map|
                   space_ids = spaces_map[:id_map].keys
                   # Set all associations to nil so if no records are found, we don't do another query when somebody tries to load the association
                   spaces_map[:rows].each { |space| space.associations[:security_groups] = [] }
                   default_security_groups = SecurityGroup.where(running_default: true).all
                   SecurityGroupsSpace.where(space_id: space_ids).eager(:security_group).all do |security_group_space|
                     space = spaces_map[:id_map][security_group_space.space_id].first
                     space.associations[:security_groups] << security_group_space.security_group
                   end
                   spaces_map[:rows].each do |space|
                     space.associations[:security_groups] += default_security_groups
                     space.associations[:security_groups].uniq!
                   end
                 }

    many_to_many :staging_security_groups,
                 class: 'VCAP::CloudController::SecurityGroup',
                 join_table: 'staging_security_groups_spaces',
                 left_key: :staging_space_id,
                 right_key: :staging_security_group_id,
                 dataset: lambda {
                   SecurityGroup.left_join(:staging_security_groups_spaces, staging_security_group_id: :id).
                     where(Sequel.or(staging_security_groups_spaces__staging_space_id: id, security_groups__staging_default: true)).distinct(:id)
                 },
                 eager_loader: lambda { |spaces_map|
                   space_ids = spaces_map[:id_map].keys
                   # Set all associations to nil so if no records are found, we don't do another query when somebody tries to load the association
                   spaces_map[:rows].each { |space| space.associations[:staging_security_groups] = [] }
                   default_security_groups = SecurityGroup.where(staging_default: true).all
                   StagingSecurityGroupsSpace.where(staging_space_id: space_ids).eager(:security_group).all do |security_group_space|
                     space = spaces_map[:id_map][security_group_space.staging_space_id].first
                     space.associations[:staging_security_groups] << security_group_space.security_group
                   end
                   spaces_map[:rows].each do |space|
                     space.associations[:staging_security_groups] += default_security_groups
                     space.associations[:staging_security_groups].uniq!
                   end
                 }

    one_to_many :app_events,
                dataset: -> { AppEvent.filter(app: apps) }

    one_to_many :default_users, class: 'VCAP::CloudController::User', key: :default_space_id

    one_to_many :domains,
                dataset: -> { organization.domains_dataset },
                adder: ->(domain) { domain.addable_to_organization!(organization) },
                eager_loader: proc { |eo|
                  id_map = {}
                  eo[:rows].each do |space|
                    space.associations[:domains] = []
                    id_map[space.organization_id] ||= []
                    id_map[space.organization_id] << space
                  end
                  ds = Domain.shared_or_owned_by(id_map.keys)
                  ds = ds.eager(eo[:associations]) if eo[:associations]
                  ds = eo[:eager_block].call(ds) if eo[:eager_block]
                  ds.all do |domain|
                    if domain.shared?
                      id_map.each_value { |spaces| spaces.each { |space| space.associations[:domains] << domain } }
                    else
                      id_map[domain.owning_organization_id].each { |space| space.associations[:domains] << domain }
                    end
                  end
                }

    many_to_one :space_quota_definition

    add_association_dependencies(
      default_users: :nullify,
      processes: :destroy,
      routes: :destroy,
      security_groups: :nullify,
      staging_security_groups: :nullify
    )

    # Unfortunately, because v2 non-recursive deletes expect labels and annotations to be
    # recursively deleted, we can't use association_dependencies like most other models.
    # The reason they are still deleted is because they would be stale metadata.
    # TODO: Change this to use add_association_dependencies when v2 is removed
    def before_destroy
      LabelDelete.delete(labels)
      AnnotationDelete.delete(annotations)
      super
    end

    export_attributes :name, :organization_guid, :space_quota_definition_guid, :allow_ssh

    import_attributes :name, :organization_guid, :developer_guids, :allow_ssh, :isolation_segment_guid,
                      :manager_guids, :auditor_guids, :supporter_guids, :security_group_guids, :space_quota_definition_guid

    strip_attributes :name

    dataset_module do
      def having_developers(*users)
        join(:spaces_developers, spaces_developers__space_id: :spaces__id).
          where(spaces_developers__user_id: users.map(&:id)).select_all(:spaces)
      end
    end

    def add_auditor(user)
      validate_auditor(user)
      SpaceAuditor.find_or_create(user_id: user.id, space_id: id)
      reload
    end

    def add_supporter(user)
      validate_supporter(user)
      SpaceSupporter.find_or_create(user_id: user.id, space_id: id)
      reload
    end

    def add_manager(user)
      validate_manager(user)
      SpaceManager.find_or_create(user_id: user.id, space_id: id)
      reload
    end

    def add_developer(user)
      validate_developer(user)
      SpaceDeveloper.find_or_create(user_id: user.id, space_id: id)
      reload
    end

    def has_developer?(user)
      user.present? && SpaceDeveloper.where(space_id: id, user_id: user.id).any?
    end

    def has_supporter?(user)
      user.present? && SpaceSupporter.where(space_id: id, user_id: user.id).any?
    end

    def has_member?(user)
      has_developer?(user) || has_manager?(user) || has_auditor?(user)
    end

    def in_organization?(user)
      organization && organization.has_user?(user)
    end

    def around_save
      yield
    rescue Sequel::UniqueConstraintViolation => e
      raise e unless e.message.include?('spaces_org_id_name_index')

      errors.add(%i[organization_id name], :unique)
      raise validation_failed_error
    end

    def validate
      validates_presence :name
      validates_presence :organization
      validates_unique %i[organization_id name]
      validates_format SPACE_NAME_REGEX, :name

      errors.add(:space_quota_definition, :invalid_organization) if space_quota_definition && space_quota_definition.organization_id != organization.id

      return unless column_changed?(:isolation_segment_guid)

      validate_isolation_segment(isolation_segment_model)
    end

    def validate_isolation_segment(isolation_segment_model)
      validate_isolation_segment_set(isolation_segment_model) if isolation_segment_model
    end

    def validate_developer(user)
      raise InvalidDeveloperRelation.new(user.guid) unless in_organization?(user)
    end

    def validate_supporter(user)
      raise InvalidSupporterRelation.new(user.guid) unless in_organization?(user)
    end

    def validate_manager(user)
      raise InvalidManagerRelation.new(user.guid) unless in_organization?(user)
    end

    def validate_auditor(user)
      raise InvalidAuditorRelation.new(user.guid) unless in_organization?(user)
    end

    def validate_change_organization(new_org)
      raise CloudController::Errors::ApiError.new_from_details('OrganizationAlreadySet') unless organization.nil? || organization.guid == new_org.guid
    end

    def find_visible_service_instance_by_name(name)
      shared = service_instances_shared_from_other_spaces_dataset.where(name:).all
      source = service_instances_dataset.where(name:).all

      (shared | source).first
    end

    def number_service_keys
      ServiceKey.join(:service_instances, id: :service_instance_id).where(service_instances__space_id: id).count
    end

    def self.user_visibility_filter(user)
      {
        spaces__id: user.space_developer_space_ids.
          union(user.space_manager_space_ids, from_self: false).
          union(user.space_auditor_space_ids, from_self: false).
          union(user.space_supporter_space_ids, from_self: false).
          union(dataset.join(user.org_manager_org_ids, organization_id: :organization_id).select(:spaces__id), from_self: false).
          select(:space_id)
      }
    end

    def has_remaining_memory(mem)
      return true unless space_quota_definition

      space_quota_definition.memory_limit == SpaceQuotaDefinition::UNLIMITED || memory_remaining >= mem
    end

    def has_remaining_log_rate_limit(log_rate_limit_desired)
      return true unless space_quota_definition

      space_quota_definition.log_rate_limit == SpaceQuotaDefinition::UNLIMITED || log_rate_limit_remaining >= log_rate_limit_desired
    end

    def instance_memory_limit
      if space_quota_definition
        space_quota_definition.instance_memory_limit
      else
        SpaceQuotaDefinition::UNLIMITED
      end
    end

    def log_rate_limit
      if space_quota_definition
        space_quota_definition.log_rate_limit
      else
        SpaceQuotaDefinition::UNLIMITED
      end
    end

    def app_task_limit
      if space_quota_definition
        space_quota_definition.app_task_limit
      else
        SpaceQuotaDefinition::UNLIMITED
      end
    end

    def meets_max_task_limit?
      app_task_limit <= running_and_pending_tasks_count
    end

    def in_suspended_org?
      organization.suspended?
    end

    def members
      User.dataset.where(id: Role.where(space_id: id).distinct.select(:user_id))
    end

    def memory_used
      started_app_memory + running_task_memory
    end

    def running_and_pending_tasks_count
      tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count
    end

    private

    def has_manager?(user)
      user.present? && SpaceManager.where(space_id: id, user_id: user.id).any?
    end

    def has_auditor?(user)
      user.present? && SpaceAuditor.where(space_id: id, user_id: user.id).any?
    end

    def memory_remaining
      space_quota_definition.memory_limit - memory_used
    end

    def log_rate_limit_remaining
      space_quota_definition.log_rate_limit - (started_app_log_rate_limit + running_task_log_rate_limit)
    end

    def running_task_memory
      tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:memory_in_mb) || 0
    end

    def started_app_memory
      processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:memory, :instances)) || 0
    end

    def running_task_log_rate_limit
      tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:log_rate_limit) || 0
    end

    def started_app_log_rate_limit
      processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:log_rate_limit, :instances)) || 0
    end

    def validate_isolation_segment_set(isolation_segment_model)
      isolation_segment_guids = organization.isolation_segment_models.map(&:guid)
      return if isolation_segment_guids.include?(isolation_segment_model.guid)

      raise CloudController::Errors::ApiError.new_from_details('UnableToPerform',
                                                               'Adding the Isolation Segment to the Space',
                                                               "Only Isolation Segments in the Organization's allowed list can be used.")
    end
  end
end
