require_relative './permission/presenter'
require_relative './visualization/member'
require_relative 'shared_entity'

module CartoDB
  class Permission < Sequel::Model

    # @param id String (uuid)
    # @param owner_id String (uuid)
    # @param owner_username String
    # @param entity_id String (uuid)
    # @param entity_type String

    # Another hack to resolve the problem with Sequel new? method
    @new_object = false

    @old_acl = nil

    ACCESS_READONLY   = 'r'.freeze
    ACCESS_READWRITE  = 'rw'.freeze
    ACCESS_NONE       = 'n'.freeze
    SORTED_ACCESSES   = [ACCESS_READWRITE, ACCESS_READONLY, ACCESS_NONE].freeze

    TYPE_USER         = 'user'
    TYPE_ORGANIZATION = 'org'
    TYPE_GROUP = 'group'

    ENTITY_TYPE_VISUALIZATION = 'vis'

    DEFAULT_ACL_VALUE = '[]'

    # Format: requested_permission => [ allowed_permissions_list ]
    PERMISSIONS_MATRIX = {
        ACCESS_READONLY    => [ ACCESS_READONLY, ACCESS_READWRITE ],
        ACCESS_READWRITE   => [ ACCESS_READWRITE ],
        ACCESS_NONE        => []
    }

    ALLOWED_ENTITY_KEYS = [:id, :username, :name, :avatar_url]

    # @return Hash
    def acl
      ::JSON.parse((self.access_control_list.nil? ? DEFAULT_ACL_VALUE : self.access_control_list), symbolize_names: true)
    end

    def real_entity_type
      entity.type
    end

    def notify_permissions_change(permissions_changes)
      begin
        permissions_changes.each do |c, v|
          # At the moment we just check users permissions
          if c == 'user'
            v.each do |affected_id, perm|
              # Perm is an array. For the moment just one type of permission can
              # be applied to a type of object. But with an array this is open
              # to more than one permission change at a time
              perm.each do |p|
                if self.real_entity_type == CartoDB::Visualization::Member::TYPE_DERIVED
                  if p['action'] == 'grant'
                    # At this moment just inform as read grant
                    if p['type'].include?('r')
                      ::Resque.enqueue(::Resque::UserJobs::Mail::ShareVisualization, self.entity.id, affected_id)
                    end
                  elsif p['action'] == 'revoke'
                    if p['type'].include?('r')
                      ::Resque.enqueue(::Resque::UserJobs::Mail::UnshareVisualization, self.entity.name, self.owner_username, affected_id)
                    end
                  end
                elsif self.real_entity_type == CartoDB::Visualization::Member::TYPE_CANONICAL
                  if p['action'] == 'grant'
                    # At this moment just inform as read grant
                    if p['type'].include?('r')
                      ::Resque.enqueue(::Resque::UserJobs::Mail::ShareTable, self.entity.id, affected_id)
                    end
                  elsif p['action'] == 'revoke'
                    if p['type'].include?('r')
                      ::Resque.enqueue(::Resque::UserJobs::Mail::UnshareTable, self.entity.name, self.owner_username, affected_id)
                    end
                  end
                end
              end
            end
          end
        end
      rescue => e
        CartoDB::Logger.error(message: "Problem sending notification mail", exception: e)
      end
    end

    def self.compare_new_acl(old_acl, new_acl)
      temp_old_acl = {}
      # Convert the old and new acls to a better format for searching
      old_acl.each do |i|
        if !temp_old_acl.has_key?(i[:type])
          temp_old_acl[i[:type]] = {}
        end
        temp_old_acl[i[:type]][i[:id]] = i
      end
      temp_new_acl = {}
      new_acl.each do |i|
        if !temp_new_acl.has_key?(i[:type])
          temp_new_acl[i[:type]] = {}
        end
        temp_new_acl[i[:type]][i[:id]] = i
      end

      # Iterate through the new acl and compare elements with the old one
      permissions_change = {}
      temp_new_acl.each do |pt, pv|
        permissions_change[pt] = {}
        pv.each do |oi, iacl|
          # See if a specific permission exists in the old acl
          # If the new acl is greater than the old we suppose that write
          # permissions were granted. Otherwise they were revoked
          # If the permissions doesn't exist in the old acl it has been granted
          # After the comparisson both old and new acl are removed from the
          # temporal structure
          if !temp_old_acl[pt].nil? && !temp_old_acl[pt][oi].nil?
            case temp_new_acl[pt][oi][:access] <=> temp_old_acl[pt][oi][:access]
            when 1
              permissions_change[pt][oi] = [{'action' => 'grant', 'type' => 'w'}]
            when -1
              permissions_change[pt][oi] = [{'action' => 'revoke', 'type' => 'w'}]
            end
            temp_old_acl[pt].delete(oi)
          else
            permissions_change[pt][oi] = [{'action' => 'grant', 'type' => temp_new_acl[pt][oi][:access]}]
          end
          temp_new_acl[pt].delete(oi)
        end
      end

      # Iterate through the old acl. All the permissions in this structure are
      # supposed so be revokes
      temp_old_acl.each do |pt, pv|
        if permissions_change[pt].nil?
          permissions_change[pt] = {}
        end
        pv.each do |oi, iacl|
          permissions_change[pt][oi] = [{'action' => 'revoke', 'type' => temp_old_acl[pt][oi][:access]}]
        end
      end

      return permissions_change
    end

    # Format:
    # [
    #   {
    #     type:         string,
    #     entity:
    #     {
    #       id:         uuid,
    #       username:   string,
    #       avatar_url: string,   (optional)
    #     },
    #     access:       string
    #   }
    # ]
    #
    # type is from TYPE_xxxxx constants
    # access is from ACCESS_xxxxx constants
    #
    # @param value Array
    # @throws PermissionError
    def acl=(value)
      incoming_acl = value.nil? ? ::JSON.parse(DEFAULT_ACL_VALUE) : value
      raise PermissionError.new('ACL is not an array') unless incoming_acl.kind_of? Array
      incoming_acl.map { |item|
        unless item.kind_of?(Hash) && acl_has_required_fields?(item) && acl_has_valid_access?(item)
          raise PermissionError.new('Wrong ACL entry format')
        end
      }

      acl_items = incoming_acl.map do |item|
        {
          type:   item[:type],
          id:     item[:entity][:id],
          access: item[:access]
        }
      end

      cleaned_acl = acl_items.select { |i| i[:id] } # Cleaning, see #5668

      if @old_acl.nil?
        @old_acl = acl
      end

      self.access_control_list = ::JSON.dump(cleaned_acl)
    end

    def set_group_permission(group, access)
      # You only want to switch it off when request is a permission request coming from database
      @update_db_group_permission = false

      granted_access = granted_access_for_group(group)
      if granted_access == access
        raise ModelAlreadyExistsError.new("Group #{group.name} already has #{access} access")
      elsif granted_access == ACCESS_NONE
        set_subject_permission(group.id, access, TYPE_GROUP)
      else
        # Remove group entry from acl in order to add new (or none if ACCESS_NONE)
        new_acl = self.inputable_acl.select { |entry| entry[:entity][:id] != group.id }

        unless access == ACCESS_NONE
          acl_entry = {
            type: TYPE_GROUP,
            entity: {
              id: group.id,
              name: group.name
            },
            access: access
          }
          new_acl << acl_entry
        end

        self.acl = new_acl
      end
    end

    def remove_group_permission(group)
      # You only want to switch it off when request is a permission request coming from database
      @update_db_group_permission = false

      set_group_permission(group, ACCESS_NONE)
    end

    def remove_user_permission(user)
      granted_access = granted_access_for_user(user)
      if granted_access != ACCESS_NONE
        self.acl = inputable_acl.select { |entry| entry[:entity][:id] != user.id }
      end
    end

    def set_user_permission(subject, access)
      set_subject_permission(subject.id, access, TYPE_USER)
    end

    # acl write method expects entries to have entity, although they're not
    # stored.
    # TODO: fix this, since this is coupled to representation.
    def inputable_acl
      self.acl.map { |entry|
        {
          type: entry[:type],
          entity: {
            id: entry[:id],
            avatar_url: '',
            username: '',
            name: ''
          },
          access: entry[:access]
        }
      }
    end

    def set_subject_permission(subject_id, access, type)
      new_acl = inputable_acl

      new_acl << {
          type: type,
          entity: {
            id: subject_id,
            avatar_url: '',
            username: '',
            name: ''
          },
          access: access
      }

      self.acl = new_acl
    end

    # @return ::User|nil
    def owner
      @owner ||= ::User[self.owner_id] # See http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/Caching.html
    end

    # @param value ::User
    def owner=(value)
      @owner = value
      self.owner_id = value.id
      self.owner_username = value.username
    end

    # @return Mixed|nil
    def entity
      @visualization ||= CartoDB::Visualization::Collection.new.fetch(permission_id: id).first
    end

    def validate
      super
      errors.add(:owner_id, 'cannot be nil') if owner_id.nil? || owner_id.empty?
      errors.add(:owner_username, 'cannot be nil') if owner_username.nil? || owner_username.empty?
      viewer_writers = users_with_permissions(ACCESS_READWRITE).select(&:viewer)
      unless viewer_writers.empty?
        errors.add(:access_control_list, "grants write to viewers: #{viewer_writers.map(&:username).join(',')}")
      end

      if new?
        errors.add(:acl, 'must be empty on initial creation') if acl.present?
      else
        validates_presence [:id]
      end
    end

    def before_save
      super
      self.updated_at = Time.now
    end

    def after_update
      if !@old_acl.nil?
        self.notify_permissions_change(CartoDB::Permission.compare_new_acl(@old_acl, self.acl))
      end
      update_shared_entities
      # Notify change, caches should be invalidated
      entity.table.update_cdb_tablemetadata if entity && entity.table
    end

    def after_destroy
      # Hack. I need to set the new acl as empty so all the old acls are
      # considered revokes
      # We need to pass the current acl as old_acl and the new_acl as something
      # empty to recreate a revoke by deletion
      self.notify_permissions_change(CartoDB::Permission.compare_new_acl(self.acl, []))
    end

    # @param subject ::User
    # @return String Permission::ACCESS_xxx
    def permission_for_user(subject)
      # Common scenario
      return ACCESS_READWRITE if is_owner?(subject)

      permission_entries = acl.select do |entry|
        (entry[:type] == TYPE_USER && entry[:id] == subject.id) ||
          (entry[:type] == TYPE_GROUP && !subject.groups.nil? && subject.groups.map(&:id).include?(entry[:id])) ||
          (entry[:type] == TYPE_ORGANIZATION && !subject.organization.nil? && subject.organization.id == entry[:id])
      end

      higher_access(permission_entries.map { |entry| entry[:access] })
    end

    def higher_access(accesses)
      return ACCESS_NONE if accesses.empty?
      index = SORTED_ACCESSES.index do |access|
        accesses.include?(access)
      end
      SORTED_ACCESSES[index]
    end

    def permission_for_org
      permission = nil
      acl.map { |entry|
        if entry[:type] == TYPE_ORGANIZATION
            permission = entry[:access]
        end
      }
      ACCESS_NONE if permission.nil?
    end

    def granted_access_for_user(user)
      granted_access_for_entry_type(TYPE_USER, user)
    end

    def granted_access_for_group(group)
      granted_access_for_entry_type(TYPE_GROUP, group)
    end

    # Note: Does not check ownership
    # @param subject ::User
    # @param access String Permission::ACCESS_xxx
    def permitted?(subject, access)
      permission = permission_for_user(subject)
      Permission::PERMISSIONS_MATRIX[access].include? permission
    end

    def is_owner?(subject)
      self.owner_id == subject.id
    end

    def to_poro
      CartoDB::PermissionPresenter.new(self).to_poro
    end

    def destroy_shared_entities
      CartoDB::SharedEntity.where(entity_id: entity.id).delete
    end

    def clear
      self.acl = []
      revoke_previous_permissions(entity)
      save
    end

    def update_shared_entities
      e = entity
      # First clean previous sharings
      destroy_shared_entities
      revoke_previous_permissions(e)

      # Create user entities for the new ACL
      users = relevant_user_acl_entries(acl)
      # Avoid entries without recipient id. See #5668.
      users.select { |u| u[:id] }.each do |user|
        shared_entity = CartoDB::SharedEntity.new(
            recipient_id:   user[:id],
            recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
            entity_id:      entity.id,
            entity_type:    CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
        ).save

        if e.table?
          grant_db_permission(e, user[:access], shared_entity)
        end
      end

      org = relevant_org_acl_entry(acl)
      if org
        shared_entity = CartoDB::SharedEntity.new(
            recipient_id:   org[:id],
            recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_ORGANIZATION,
            entity_id:      entity.id,
            entity_type:    CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
        ).save

        if e.table?
          grant_db_permission(e, org[:access], shared_entity)
        end
      end

      groups = relevant_groups_acl_entries(acl)
      groups.each do |group|
        CartoDB::SharedEntity.new(
            recipient_id:   group[:id],
            recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_GROUP,
            entity_id:      entity.id,
            entity_type:    CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
        ).save

        # You only want to switch it off when request is a permission request coming from database
        if e.table? && @update_db_group_permission != false
          Carto::Group.find(group[:id]).grant_db_permission(e.table, group[:access])
        end
      end

      e.invalidate_for_permissions_change
    end

    def users_with_permissions(access)
      user_ids = relevant_user_acl_entries(acl).select { |e| access == e[:access] }.map { |e| e[:id] }
      ::User.where(id: user_ids).all
    end

    def entity_type
      ENTITY_TYPE_VISUALIZATION
    end

    def entity_id
      entity.id
    end

    private

    def granted_access_for_entry_type(type, entity)
      permission = nil

      acl.map do |entry|
        if entry[:type] == type && entry[:id] == entity.id
          permission = entry[:access]
        end
      end
      permission = ACCESS_NONE if permission.nil?
      permission
    end

    # when removing permission form a table related visualizations should
    # be checked. The policy is the following:
    #  - if the table is used in one layer of the visualization, it's removed
    #  - if the table is used in the only one visualization layer, the vis is removed
    # TODO: send a notification to the visualizations owner
    def check_related_visualizations(table)
      fully_dependent_visualizations = table.fully_dependent_visualizations.to_a
      partially_dependent_visualizations = table.partially_dependent_visualizations.to_a
      table_visualization = table.table_visualization
      partially_dependent_visualizations.each do |visualization|
        # check permissions, if the owner does not have permissions
        # to see the table the layers using this table are removed
        perm = visualization.permission
        unless table_visualization.has_permission?(perm.owner, CartoDB::Visualization::Member::PERMISSION_READONLY)
          visualization.unlink_from(table)
        end
      end

      fully_dependent_visualizations.each do |visualization|
        # check permissions, if the owner does not have permissions
        # to see the table the visualization is removed
        perm = visualization.permission
        unless table_visualization.has_permission?(perm.owner, CartoDB::Visualization::Member::PERMISSION_READONLY)
          visualization.delete
        end
      end
    end

    def revoke_previous_permissions(entity)
      users = relevant_user_acl_entries(@old_acl.nil? ? [] : @old_acl)
      org = relevant_org_acl_entry(@old_acl.nil? ? [] : @old_acl)
      groups = relevant_groups_acl_entries(@old_acl.nil? ? [] : @old_acl)
      case entity.class.name
        when CartoDB::Visualization::Member.to_s
          if entity.table
            if org
              entity.table.remove_organization_access
            end
            users.each { |user|
              # Cleaning, see #5668
              u = ::User[user[:id]]
              entity.table.remove_access(u) if u
            }
            # update_db_group_permission check is needed to avoid updating db requests
            if @update_db_group_permission != false
              groups.each { |group|
                Carto::Group.find(group[:id]).grant_db_permission(entity.table, ACCESS_NONE)
              }
            end
            check_related_visualizations(entity.table)
          end
        else
          raise PermissionError.new("Unsupported entity type trying to grant permission: #{entity.class.name}")
      end
    end

    def grant_db_permission(entity, access, shared_entity)
      if shared_entity.recipient_type == CartoDB::SharedEntity::RECIPIENT_TYPE_ORGANIZATION
        permission_strategy = OrganizationPermission.new
      else
        u = ::User.where(id: shared_entity[:recipient_id]).first
        permission_strategy = UserPermission.new(u)
      end

      case entity.class.name
        when CartoDB::Visualization::Member.to_s
          # assert database permissions for non canonical tables are assigned
          # its canonical vis
          if not entity.table
              raise PermissionError.new('Trying to change permissions to a table without ownership')
          end
          table = entity.table

          # check ownership
          if not self.owner_id == entity.permission.owner_id
            raise PermissionError.new('Trying to change permissions to a table without ownership')
          end
          # give permission
          if access == ACCESS_READONLY
            permission_strategy.add_read_permission(table)
          elsif access == ACCESS_READWRITE
            permission_strategy.add_read_write_permission(table)
          end
        else
          raise PermissionError.new('Unsupported entity type trying to grant permission')
      end
    end

    # Only user entries, and those with forbids also skipped
    def relevant_user_acl_entries(acl_list)
      relevant_acl_entries(acl_list, TYPE_USER)
    end

    def relevant_org_acl_entry(acl_list)
      relevant_acl_entries(acl_list, TYPE_ORGANIZATION).first
    end

    def relevant_groups_acl_entries(acl_list)
      relevant_acl_entries(acl_list, TYPE_GROUP)
    end

    def relevant_acl_entries(acl_list, type)
      acl_list.select { |entry|
        entry[:type] == type && entry[:access] != ACCESS_NONE
      }.map { |entry|
        {
            id:     entry[:id],
            access: entry[:access]
        }
      }
    end

    def acl_has_required_fields?(acl_item)
      acl_item[:entity].present? && acl_item[:type].present? && acl_item[:access].present? && acl_has_valid_entity_field?(acl_item)
    end

    def acl_has_valid_entity_field?(acl_item)
      acl_item[:entity].keys - ALLOWED_ENTITY_KEYS == []
    end

    def acl_has_valid_access?(acl_item)
      valid_access = [ACCESS_READONLY, ACCESS_NONE]
      if entity.table?
        valid_access << ACCESS_READWRITE
      end
      valid_access.include?(acl_item[:access])
    end

  end

  class PermissionError < StandardError; end


  class OrganizationPermission
    def add_read_permission(table)
      table.add_organization_read_permission
    end

    def add_read_write_permission(table)
      table.add_organization_read_write_permission
    end

    def is_permitted(table, access)
      table.permission.permission_for_org == access
    end
  end


  class UserPermission

    def initialize(user)
      @user = user
    end

    def is_permitted(table, access)
      table.permission.permitted?(@user, access)
    end

    def add_read_permission(table)
      table.add_read_permission(@user)
    end

    def add_read_write_permission(table)
      table.add_read_write_permission(@user)
    end
  end

end
