#--
# Copyright 2025 grit42 A/S. <https://grit42.com/>
#
# This file is part of grit-core.
#
# grit-core 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  any later version.
#
# grit-core 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
# grit-core. If not, see <https://www.gnu.org/licenses/>.
#++

module Grit::Core
  class User < ApplicationRecord
    include Grit::Core::GritEntityRecord

    self.table_name = "grit_core_users"
    self.sequence_name = :autogenerated


    display_columns [ "name", "login" ]
    entity_crud_with read: [], create: [ "Administrator" ], update: [ "Administrator" ], destroy: [ "Administrator" ]

    EMAIL = /
    \A
    [A-Z0-9_.&%+\-']+   # mailbox
    @
    (?:[A-Z0-9-]+\.)+  # subdomains
    (?:[A-Z]{2,25})     # TLD
    \z
  /ix
    LOGIN = /\A[a-zA-Z0-9_][a-zA-Z0-9.+\-_@ ]+\z/

    validates :email,
              format: {
                with: EMAIL,
                message: proc {
                  ::Authlogic::I18n.t(
                    "error_messages.email_invalid",
                    default: "should look like an email address."
                  )
                }
              },
              length: { maximum: 100 },
              uniqueness: {
                case_sensitive: false,
                if: :will_save_change_to_email?
              }

    validates :login,
              format: {
                with: LOGIN,
                message: proc {
                  ::Authlogic::I18n.t(
                    "error_messages.login_invalid",
                    default: "should use only letters, numbers, spaces, and .-_@+ please."
                  )
                }
              },
              length: { within: 3..100 },
              uniqueness: {
                case_sensitive: false,
                if: :will_save_change_to_login?
              }

    validates :password,
              confirmation: { if: :require_password? },
              length: {
                minimum: 8,
                if: :require_password?
              }
    validates :password_confirmation,
              length: {
                minimum: 8,
                if: :require_password?
              }

    before_validation :random_password, on: :create
    before_create :set_default_values
    before_create :check_user
    before_update :check_who
    before_update :check_admin_active
    before_update :validate_fields
    before_update :validate_two_factor
    after_update  :new_session
    before_destroy :check_dependencies

    self.belongs_to_required_by_default = false
    belongs_to :origin
    belongs_to :location
    has_many :user_roles, dependent: :destroy
    has_many :roles, through: :user_roles

    @no_show = [  "crypted_password",
    "password_salt",
    "persistence_token",
    "perishable_token",
    "login_count",
    "failed_login_count",
    "last_request_at",
    "current_login_at",
    "last_login_at",
    "current_login_ip",
    "last_login_ip",
    "two_factor_token",
    "two_factor_expiry"
  ]

    def self.entity_properties(**args)
      @entity_properties ||= self.db_properties.reject { |p| @no_show.include?(p[:name]) }
    end

    def self.entity_columns(**args)
      @entity_columns ||= self.entity_columns_from_properties(self.entity_properties)
    end

    def self.entity_fields(**args)
      @entity_fields ||= self.entity_fields_from_properties(self.entity_properties).reject { |f| f[:name] == "settings" }
    end

    def self.detailed(params = nil)
      query = self.from(self.table_name)
      self.columns.each do |column|
        next if @no_show.include?(column.name)
        query = query.select("#{self.table_name}.#{column.name}")
      end

      self.foreign_keys.each do |foreign_key, memo|
        foreign_key_model = Grit::Core::EntityMapper.table_to_model_name(foreign_key.to_table).constantize
        query = query.joins("LEFT OUTER JOIN #{foreign_key.to_table} #{foreign_key.to_table}__ ON #{foreign_key.to_table}__.#{foreign_key.options[:primary_key]} = #{self.table_name}.#{foreign_key.options[:column]}")
        foreign_key_model.display_properties.each do |property|
          next if @no_show.include?(foreign_key.options[:column])
          query = query.select("#{foreign_key.to_table}__.#{property[:name]} as #{foreign_key.options[:column]}__#{property[:name]}")
        end
      end
      query
    end

    def self.permitted_params
      %i[login name email origin_id location_id password password_confirmation settings status_id
         auth_method two_factor profile_picture active]
    end

    acts_as_authentic do |c|
      c.crypto_provider = Authlogic::CryptoProviders::SCrypt
      c.log_in_after_create = false
    end

    def active?
      active || %w[true 1].include?(active.to_s.downcase)
    end

    def self.current
      unless RequestStore.store["current_user"]
        user_session = Grit::Core::UserSession.find
        raise "Unknown login or wrong password entered for login" unless user_session

        RequestStore.store["current_user"] = find(user_session.record.id)
      end
      RequestStore.store["current_user"]
    end

    def role?(role_name = nil)
      Grit::Core::Role.access?(role_name: role_name)
    end

    def one_of_these_roles?(roles = [])
      RequestStore.store["one_of_these_roles#{roles.join('_')}"] ||= self.roles.where(name: roles).count
      count = RequestStore.store["one_of_these_roles#{roles.join('_')}"]
      count.positive?
    end

    def self.user_administration(params = nil)
      return nil unless Grit::Core::User.current.role?("Administrator")

      self.detailed
      .select("(
        SELECT array_agg(user_roles__.role_id) FROM (
          SELECT role_id FROM grit_core_user_roles WHERE grit_core_user_roles.user_id = grit_core_users.id
        ) as user_roles__
      ) as role_ids")
      .select("grit_core_users.active")
    end

    private

      def check_role
        return if Grit::Core::User.current.role?("Administrator")

        raise "Administrator role required to manage users"
      end

      def check_user
        return if created_by == "admin"

        check_role
      end

      def set_default_values
        self.login_count = 0
        self.failed_login_count = 0
        self.login = login.downcase
        self.email = email.downcase
        return if settings.present?

        self.settings = { "theme": "dark" }
      end

      def check_who
        # It is only admin and the user self that can edit user accounts
        # Exception when activating process is in progress
        raise "Not allowed" unless forgot_token_was.blank? || (forgot_token.blank? && !forgot_token_was.nil?) || activation_token_was.blank? || (activation_token.blank? && !activation_token_was.nil?) || Grit::Core::User.current.role?("Administrator") || (login == Grit::Core::User.current.login)

        true
      end

      def check_admin_active
        # It is not allowed to disable the admin user
        return unless login == "admin" && active == false

        raise "Not allowed"
      end

      def validate_fields
        # It is not allowed to change the login field
        return unless will_save_change_to_login? && login_was.present?

        raise "It is not allowed to change the login field"
      end

      def validate_two_factor
        return unless two_factor == true && email.blank?

        raise "Two factor authentication needs an email address"
      end

      def new_session
        return unless password && login

        user_session = Grit::Core::UserSession.new(login: login, password: password)
        user_session.save
      end

      def check_dependencies
        check_role
        return unless login == "admin"

        raise "Not allowed"
      end

      def random_password
        @rand_password = SecureRandom.urlsafe_base64(8)
        self.password = @rand_password
        self.password_confirmation = @rand_password
      end
  end
end
