require 'nostr'

class User < ApplicationRecord
  include EmailValidatable

  attr_accessor :display_name
  attr_accessor :avatar_new
  attr_accessor :current_password

  serialize :preferences, coder: UserPreferences

  #
  # Relations
  #

  has_many :invitations, dependent: :destroy
  has_one  :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
  has_one  :inviter, through: :invitation, source: :user
  has_many :invitees, through: :invitations

  has_many :donations, dependent: :nullify

  has_one  :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
                         primary_key: "ln_account", foreign_key: "login"

  has_many :accounts, through: :lndhub_user

  has_many :remote_storage_authorizations

  #
  # Validations
  #

  validates_uniqueness_of :cn, scope: :ou
  validates_length_of :cn, minimum: 3
  validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
                           if: Proc.new{ |u| u.cn.present? },
                           message: "is invalid. Please use only letters, numbers and -"
  validates_format_of :cn, without: /\A-/,
                           if: Proc.new{ |u| u.cn.present? },
                           message: "is invalid. Usernames need to start with a letter."
  # FIXME This needs a server restart to apply values
  validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
                           message: "has already been taken",
                           unless: Proc.new{ |u| u.persisted? }

  validates_uniqueness_of :email
  validates :email, email: true

  validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
                                     if: -> { defined?(@display_name) }

  validates_uniqueness_of :nostr_pubkey, allow_blank: true

  validate :acceptable_avatar

  #
  # Scopes
  #

  scope :confirmed,  -> { where.not(confirmed_at: nil) }
  scope :pending,    -> { where(confirmed_at: nil) }
  scope :all_except, -> (user) { where.not(id: user) }

  #
  # Encrypted database columns
  #

  has_encrypted :ln_login, :ln_password

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :ldap_authenticatable,
         :confirmable,
         :recoverable,
         :validatable,
         :timeoutable,
         :rememberable

  def ldap_before_save
    self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
    self.ou    = dn.split(',')
                   .select{|e| e[0..1] == "ou"}.first
                   .delete_prefix("ou=")

    if self.confirmed_at.blank? && self.confirmation_token.blank?
      # User had an account with a trusted email address before akkounts was a thing
      self.confirmed_at = DateTime.now
    end
  end

  def devise_after_confirmation
    if ldap_entry[:mail] != self.email
      # E-Mail update confirmed
      LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
    else
      # E-Mail from signup confirmed (i.e. account activation)

      # TODO Make configurable, only activate globally enabled services
      enable_service %w[ discourse gitea mediawiki xmpp ]

      # TODO enable in development when we have easy setup of ejabberd etc.
      return if Rails.env.development? || !Setting.ejabberd_enabled?

      XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
      XmppSetDefaultBookmarksJob.perform_later(self)
    end
  end

  def send_devise_notification(notification, *args)
    devise_mailer.send(notification, self, *args).deliver_later
  end

  def reset_password(new_password, new_password_confirmation)
    self.password = new_password
    self.password_confirmation = new_password_confirmation
    return false unless valid?

    Devise::LDAP::Adapter.update_password(login_with, new_password)
    clear_reset_password_token
    save
  end

  def is_admin?
    admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
                !!admin.first
              else
                false
              end
  end

  def address
    "#{self.cn}@#{self.ou}"
  end

  def mastodon_address
    return nil unless Setting.mastodon_enabled?
    "#{self.cn}@#{Setting.mastodon_address_domain}"
  end

  def valid_attribute?(attribute_name)
    self.valid?
    self.errors[attribute_name].blank?
  end

  def ln_create_invoice(payload)
    lndhub = Lndhub.new
    lndhub.authenticate self
    lndhub.addinvoice payload
  end

  def dn
    return @dn if defined?(@dn)
    @dn = Devise::LDAP::Adapter.get_dn(self.cn)
  end

  def ldap_entry(reload: false)
    return @ldap_entry if defined?(@ldap_entry) && !reload
    @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first
  end

  def display_name
    @display_name ||= ldap_entry[:display_name]
  end

  def avatar
    @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
  end

  def services_enabled
    ldap_entry[:service] || []
  end

  def enable_service(service)
    current_services = services_enabled
    new_services = Array(service).map(&:to_s)
    services = (current_services + new_services).uniq
    ldap.replace_attribute(dn, :service, services)
  end

  def disable_service(service)
    current_services = services_enabled
    disabled_services = Array(service).map(&:to_s)
    services = (current_services - disabled_services).uniq
    ldap.replace_attribute(dn, :service, services)
  end

  def disable_all_services
    ldap.delete_attribute(dn,:service)
  end

  def nostr_pubkey_bech32
    return nil unless nostr_pubkey.present?
    Nostr::PublicKey.new(nostr_pubkey).to_bech32
  end

  private

    def ldap
      return @ldap_service if defined?(@ldap_service)
      @ldap_service = LdapService.new
    end

    def acceptable_avatar
      return unless avatar_new.present?

      if avatar_new.size > 1.megabyte
        errors.add(:avatar, "file size is too large")
      end

      acceptable_types = ["image/jpeg", "image/png"]
      unless acceptable_types.include?(avatar_new.content_type)
        errors.add(:avatar, "must be a JPEG or PNG file")
      end
    end
end
