require 'active_record'
require_dependency 'cartodb/errors'
require_dependency 'carto/helpers/auth_token_generator'
require_relative 'paged_model'

module Carto

  # Groups are created by the editor because of extension requests, so
  # standard Rails operations (creation, destruction, etc) doesn't trigger
  # extension management functions. In order to keep extension and database
  # in sync, there're several methods that do trigger it:
  # - create_group_with_extension
  # - rename_group_with_extension
  # - destroy_group_with_extension
  # - add_users_with_extension
  # - remove_users_with_extension
  class Group < ActiveRecord::Base
    include PagedModel
    include AuthTokenGenerator

    belongs_to :organization, class_name: Carto::Organization
    has_many :users_group, dependent: :destroy, class_name: Carto::UsersGroup
    has_many :users, through: :users_group

    private_class_method :new

    validates :name, :database_role, :organization, presence: true

    # In order to avoid locks between CDB_Organization_Remove_Organization_Access_Permission and CDB_Group_DropGroup
    # the "shared with" deletion must be performed outside the transaction, on deletion.
    after_commit :destroy_shared_with

    # Constructor for groups already existing in the database
    def self.new_instance(database_name, name, database_role, display_name = name)
      # rubocop:disable Rails/DynamicFindBy
      organization = Carto::Organization.find_by_database_name(database_name)
      # rubocop:enable Rails/DynamicFindBy

      raise "Organization not found for database #{database_name}" unless organization

      raise CartoDB::ModelAlreadyExistsError if Group.find_by_organization_id_and_name_and_database_role(organization.id, name, database_role)

      new(name: name, database_role: database_role, display_name: display_name, organization: organization)
    end

    # Creation of brand-new group with the extension
    def self.create_group_with_extension(organization, display_name)
      name = valid_group_name(display_name)
      organization.owner.in_database do |conn|
        create_group_extension_query(conn, name)
      end
      # Extension triggers a request to the editor databases endpoint which actually creates the group
      group = Carto::Group.find_by_organization_id_and_name(organization.id, name)
      raise "Group was not created by the extension. Is it installed and configured?" if group.nil?

      group.display_name = display_name
      group.save
      group
    end

    # Constructor for groups metadata, ignores database roles. Should be generally avoided.
    def self.new_instance_without_validation(name:, database_role:, display_name: name, auth_token: nil)
      new(
        name: name,
        display_name: display_name,
        database_role: database_role,
        auth_token: auth_token
      )
    end

    def rename_group_with_extension(new_display_name)
      raise CartoDB::ModelAlreadyExistsError if Group.find_by_organization_id_and_display_name(organization.id, new_display_name)

      new_name = Carto::Group.valid_group_name(new_display_name)
      organization.owner.in_database do |conn|
        Carto::Group.rename_group_extension_query(conn, name, new_name)
      end
      reload
      self.display_name = new_display_name
      save
    end

    # INFO: public because it's called by Organization.
    def destroy_group_with_extension
      # INFO: currently only a superuser can destroy a group. See CartoDB/cartodb-postgresql#114
      organization.owner.in_database(as: :superuser) do |conn|
        Carto::Group.destroy_group_extension_query(conn, name)
      end
    end

    def add_users_with_extension(users)
      organization.owner.in_database do |conn|
        Carto::Group.add_users_group_extension_query(conn, name, users.collect(&:username))
      end
      reload
    end

    def remove_users_with_extension(users)
      organization.owner.in_database do |conn|
        Carto::Group.remove_users_group_extension_query(conn, name, users.collect(&:username))
      end
      reload
    end

    def grant_db_permission(table, access)
      table.owner.in_database do |conn|
        case access
        when Carto::Permission::ACCESS_NONE
          Carto::Group.revoke_all(conn, name, table.database_schema, table.name)
        when Carto::Permission::ACCESS_READONLY
          Carto::Group.grant_read(conn, name, table.database_schema, table.name)
        when Carto::Permission::ACCESS_READWRITE
          Carto::Group.grant_write(conn, name, table.database_schema, table.name)
        else
          raise "Unknown access: #{access}"
        end
      end
    end

    def rename(new_name, new_database_role)
      self.name = new_name
      self.database_role = new_database_role
    end

    def add_user(username)
      user = Carto::User.find_by_username(username)

      raise "User #{username} not found" unless user

      raise CartoDB::ModelAlreadyExistsError unless users_group.where(user_id: user.id, group_id: id).first.nil?

      user_group = Carto::UsersGroup.new(user: user, group: self)
      users_group << user_group
      user_group
    end

    def remove_user(username)
      user = Carto::User.find_by_username(username)

      raise "User #{username} not found" unless user

      if users.include?(user)
        users.destroy(user)
        user
      end
    end

    def database_name
      organization.database_name
    end

    private

    def destroy_shared_with
      if transaction_include_any_action?([:destroy])
        Carto::SharedEntity.where(recipient_id: id).find_each do |se|
          viz = Carto::Visualization.find(se.entity_id)
          permission = viz.permission
          permission.remove_group_permission(self)
          permission.save
        end
      end
    end

    # TODO: PG Format("%I", strvar); ?
    def self.valid_group_name(display_name)
      name = display_name.squish
      name = "g_#{name}" unless name[/^[a-zA-Z_]{1}/]
      name.gsub(/[^a-zA-Z0-9_ ]/, '_').gsub(/_{2,}/, '_')
    end

    def self.create_group_extension_query(conn, name)
      conn.execute(%{ select cartodb.CDB_Group_CreateGroup('#{name}') })
    end

    def self.rename_group_extension_query(conn, name, new_name)
      conn.execute(%{ select cartodb.CDB_Group_RenameGroup('#{name}', '#{new_name}') })
    end

    def self.destroy_group_extension_query(conn, name)
      conn.execute(%{ select cartodb.CDB_Group_DropGroup('#{name}') })
    end

    def self.add_users_group_extension_query(conn, name, usernames)
      conn.execute(%{ select cartodb.CDB_Group_AddUsers('#{name}', ARRAY['#{usernames.join("','")}']) })
    end

    def self.remove_users_group_extension_query(conn, name, usernames)
      conn.execute(%{ select cartodb.CDB_Group_RemoveUsers('#{name}', ARRAY['#{usernames.join("','")}']) })
    end

    def self.revoke_all(conn, group_name, table_database_schema, table_name)
      conn.execute(%{ select cartodb._CDB_Group_Table_RevokeAll('#{group_name}', '#{table_database_schema}', '#{table_name}', false) })
    end

    def self.grant_read(conn, group_name, table_database_schema, table_name)
      conn.execute(%{ select cartodb._CDB_Group_Table_GrantRead('#{group_name}', '#{table_database_schema}', '#{table_name}', false) })
    end

    def self.grant_write(conn, group_name, table_database_schema, table_name)
      conn.execute(%{ select cartodb._CDB_Group_Table_GrantReadWrite('#{group_name}', '#{table_database_schema}', '#{table_name}', false) })
    end

  end
end
