class Domain < ApplicationRecord
  include UserEvents
  include Roids
  include Versions # version/domain_version.rb
  include Domain::Expirable
  include Domain::Activatable
  include Domain::ForceDelete
  include Domain::Discardable
  include Domain::Deletable
  include Domain::Transferable
  include Domain::RegistryLockable
  include Domain::Releasable
  include Domain::Disputable
  include Domain::BulkUpdatable

  attr_accessor :roles

  attr_accessor :legal_document_id

  alias_attribute :on_hold_time, :outzone_at
  alias_attribute :outzone_time, :outzone_at
  alias_attribute :auth_info, :transfer_code # Old attribute name; for PaperTrail
  alias_attribute :registered_at, :created_at

  # TODO: whois requests ip whitelist for full info for own domains and partial info for other domains
  # TODO: most inputs should be trimmed before validatation, probably some global logic?

  belongs_to :registrar, required: true
  belongs_to :registrant, required: true
  # TODO: should we user validates_associated :registrant here?

  has_many :admin_domain_contacts
  accepts_nested_attributes_for :admin_domain_contacts,  allow_destroy: true, reject_if: :admin_change_prohibited?
  has_many :tech_domain_contacts
  accepts_nested_attributes_for :tech_domain_contacts, allow_destroy: true, reject_if: :tech_change_prohibited?

  def registrant_change_prohibited?
    statuses.include? DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED
  end


  # NB! contacts, admin_contacts, tech_contacts are empty for a new record
  has_many :domain_contacts, dependent: :destroy
  has_many :contacts, through: :domain_contacts, source: :contact
  has_many :admin_contacts, through: :admin_domain_contacts, source: :contact
  has_many :tech_contacts, through: :tech_domain_contacts, source: :contact
  has_many :nameservers, dependent: :destroy, inverse_of: :domain

  accepts_nested_attributes_for :nameservers, allow_destroy: true,
                                              reject_if: proc { |attrs| attrs[:hostname].blank? }

  has_many :transfers, class_name: 'DomainTransfer', dependent: :destroy

  has_many :dnskeys, dependent: :destroy

  has_one  :whois_record # destroyment will be done in after_commit

  accepts_nested_attributes_for :dnskeys, allow_destroy: true

  has_many :legal_documents, as: :documentable
  has_many :registrant_verifications, dependent: :destroy
  has_one :csync_record, dependent: :destroy

  after_initialize do
    self.pending_json = {} if pending_json.blank?
    self.statuses = [] if statuses.nil?
    self.status_notes = {} if status_notes.nil?
  end

  before_save :manage_automatic_statuses
  before_save :touch_always_version
  def touch_always_version
    self.updated_at = Time.zone.now
  end

  before_update :manage_statuses
  def manage_statuses
    return unless will_save_change_to_registrant_id? # rollback has not yet happened

    pending_update! if registrant_verification_asked?
    true
  end

  after_commit :update_whois_record

  after_create :update_reserved_domains
  def update_reserved_domains
    ReservedDomain.new_password_for(name) if in_reserved_list?
  end

  validates :name_dirty, domain_name: true, uniqueness: true
  validates :puny_label, length: { maximum: 63 }
  validates :period, presence: true, numericality: { only_integer: true }
  validates :transfer_code, presence: true
  validate :validate_reservation

  def validate_reservation
    return if persisted? || !in_reserved_list?

    if reserved_pw.blank?
      errors.add(:base, :required_parameter_missing_reserved)
      return false
    end

    return if ReservedDomain.pw_for(name) == reserved_pw

    errors.add(:base, :invalid_auth_information_reserved)
  end

  validate :status_is_consistant
  def status_is_consistant
    has_error = (hold_status? && statuses.include?(DomainStatus::SERVER_MANUAL_INZONE))
    unless has_error
      if (statuses & DELETE_STATUSES).any?
        has_error = statuses.include? DomainStatus::SERVER_DELETE_PROHIBITED
      end
    end
    errors.add(:domains, I18n.t(:object_status_prohibits_operation)) if has_error
  end

  attr_accessor :is_admin

  # Removed to comply new ForceDelete procedure
  # at https://github.com/internetee/registry/issues/1428#issuecomment-570561967
  #
  # validate :check_permissions, :unless => :is_admin
  # def check_permissions
  #   return unless force_delete_scheduled?
  #   errors.add(:base, I18n.t(:object_status_prohibits_operation))
  #   false
  # end

  validates :nameservers, domain_nameserver: {
    min: -> { Setting.ns_min_count },
    max: -> { Setting.ns_max_count }
  }

  validates :dnskeys, object_count: {
    min: -> { Setting.dnskeys_min_count },
    max: -> { Setting.dnskeys_max_count }
  }

  validates :admin_domain_contacts, object_count: {
    min: -> { Setting.admin_contacts_min_count },
    max: -> { Setting.admin_contacts_max_count }
  }

  validates :tech_domain_contacts, object_count: {
    min: -> { Setting.tech_contacts_min_count },
    max: -> { Setting.tech_contacts_max_count }
  }

  validates :nameservers, uniqueness_multi: {
    attribute: 'hostname'
  }

  validates :tech_domain_contacts, uniqueness_multi: {
    attribute: 'contact_code_cache'
  }

  validates :admin_domain_contacts, uniqueness_multi: {
    attribute: 'contact_code_cache'
  }

  validates :dnskeys, uniqueness_multi: {
    attribute: 'public_key'
  }

  validate :validate_nameserver_ips

  validate :statuses_uniqueness

  def security_level_resolver
    resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
    resolver.do_validation = true
    resolver.do_caching = false
    resolver.dnssec = true
    resolver
  end

  def dnssec_security_level(stubber: nil)
    Dnsruby::Dnssec.reset
    resolver = security_level_resolver
    Dnsruby::Recursor.clear_caches(resolver)
    if Rails.env.staging?
      clear_dnssec_trusted_anchors_and_keys
    elsif stubber
      Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
    end
    recursor = Dnsruby::Recursor.new(resolver)
    recursor.dnssec = true
    recursor.query(name, 'A', 'IN').security_level
  end

  def clear_dnssec_trusted_anchors_and_keys
    Dnsruby::Dnssec.clear_trust_anchors
    Dnsruby::Dnssec.clear_trusted_keys
    Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
  end

  def statuses_uniqueness
    return if statuses.uniq == statuses
    errors.add(:statuses, :taken)
  end

  attr_accessor :registrant_typeahead, :update_me,
    :epp_pending_update, :epp_pending_delete, :reserved_pw

  self.ignored_columns = %w[legacy_id legacy_registrar_id legacy_registrant_id]

  def subordinate_nameservers
    nameservers.select { |x| x.hostname.end_with?(name) }
  end

  def delegated_nameservers
    nameservers.select { |x| !x.hostname.end_with?(name) }
  end

  def admin_change_prohibited?
    statuses.include? DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED
  end

  def tech_change_prohibited?
    statuses.include? DomainStatus::SERVER_TECH_CHANGE_PROHIBITED
  end

  class << self
    def nameserver_required?
      Setting.nameserver_required
    end

    def registrant_user_admin_registrant_domains(registrant_user)
      companies = Contact.registrant_user_company_contacts(registrant_user)
      from(
        "(#{registrant_user_administered_domains(registrant_user).to_sql} UNION " \
        "#{registrant_user_company_registrant(companies).to_sql} UNION " \
        "#{registrant_user_domains_company(companies, except_tech: true).to_sql}) AS domains"
      )
    end

    def registrant_user_direct_admin_registrant_domains(registrant_user)
      from(
        "(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
        "#{registrant_user_direct_domains_by_contact(registrant_user,
                                                     except_tech: true).to_sql}) AS domains"
      )
    end

    def registrant_user_domains(registrant_user)
      from(
        "(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
        "#{registrant_user_indirect_domains(registrant_user).to_sql} UNION " \
        "#{registrant_user_domains_by_contact(registrant_user).to_sql}) AS domains"
      )
    end

    def registrant_user_direct_domains(registrant_user)
      from(
        "(#{registrant_user_direct_domains_by_registrant(registrant_user).to_sql} UNION " \
        "#{registrant_user_direct_domains_by_contact(registrant_user).to_sql}) AS domains"
      )
    end

    def registrant_user_administered_domains(registrant_user)
      from(
        "(#{registrant_user_domains_by_registrant(registrant_user).to_sql} UNION " \
        "#{registrant_user_domains_by_admin_contact(registrant_user).to_sql}) AS domains"
      )
    end

    def registrant_user_indirect_domains(registrant_user)
      companies = Contact.registrant_user_company_contacts(registrant_user)
      from(
        "(#{registrant_user_company_registrant(companies).to_sql} UNION "\
        "#{registrant_user_domains_company(companies).to_sql}) AS domains"
      )
    end

    private

    def registrant_user_domains_by_registrant(registrant_user)
      where(registrant: registrant_user.contacts)
    end

    def registrant_user_domains_by_contact(registrant_user)
      joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts })
    end

    def registrant_user_domains_by_admin_contact(registrant_user)
      joins(:domain_contacts).where(domain_contacts: { contact_id: registrant_user.contacts,
                                                       type: [AdminDomainContact.name] })
    end

    def registrant_user_direct_domains_by_registrant(registrant_user)
      where(registrant: registrant_user.direct_contacts)
    end

    def registrant_user_direct_domains_by_contact(registrant_user, except_tech: false)
      request = { contact_id: registrant_user.direct_contacts }
      request[:type] = [AdminDomainContact.name] if except_tech
      joins(:domain_contacts).where(domain_contacts: request)
    end

    def registrant_user_company_registrant(companies)
      where(registrant: companies)
    end

    def registrant_user_domains_company(companies, except_tech: false)
      request = { contact: companies }
      request[:type] = [AdminDomainContact.name] if except_tech
      joins(:domain_contacts).where(domain_contacts: request)
    end
  end

  def name=(value)
    value&.strip!
    value&.downcase!
    self[:name] = SimpleIDN.to_unicode(value)
    self[:name_puny] = SimpleIDN.to_ascii(value)
    self[:name_dirty] = value
  end

  # find by internationalized domain name
  # internet domain name => ascii or puny, but db::domains.name is unicode
  def self.find_by_idn(name)
    domain = self.find_by_name name
    if domain.blank? && name.include?('-')
      unicode = SimpleIDN.to_unicode name # we have no index on domains.name_puny
      domain = self.find_by_name unicode
    end
    domain
  end

  def puny_label
    name_puny.to_s.split('.').first
  end

  def registrant_typeahead
    @registrant_typeahead || registrant.try(:name) || nil
  end

  def in_reserved_list?
    @in_reserved_list ||= ReservedDomain.by_domain(name).any?
  end

  def pending_transfer
    transfers.find_by(status: DomainTransfer::PENDING)
  end

  def server_holdable?
    return false if statuses.include?(DomainStatus::SERVER_HOLD)
    return false if statuses.include?(DomainStatus::SERVER_MANUAL_INZONE)

    true
  end

  def renewable?
    return false unless renew_blocking_statuses.empty?
    return true unless Setting.days_to_renew_domain_before_expire != 0

    # if you can renew domain at days_to_renew before domain expiration
    if (expire_time.to_date - Time.zone.today) + 1 > Setting.days_to_renew_domain_before_expire
      return false
    end

    true
  end

  def renew_blocking_statuses
    disallowed = [DomainStatus::DELETE_CANDIDATE, DomainStatus::PENDING_RENEW,
                  DomainStatus::PENDING_TRANSFER, DomainStatus::CLIENT_RENEW_PROHIBITED,
                  DomainStatus::PENDING_UPDATE, DomainStatus::SERVER_RENEW_PROHIBITED,
                  DomainStatus::PENDING_DELETE_CONFIRMATION]

    (statuses & disallowed)
  end

  def notify_registrar(message_key)
    # TODO: To be deleted with DomainDeleteConfirm refactoring
    registrar.notifications.create!(
      text: "#{I18n.t(message_key)}: #{name}",
      attached_obj_id: id,
      attached_obj_type: self.class.to_s
    )
  end

  def preclean_pendings
    # TODO: To be deleted with refactoring
    self.registrant_verification_token = nil
    self.registrant_verification_asked_at = nil
  end

  def clean_pendings!
    # TODO: To be deleted with refactoring
    preclean_pendings
    self.pending_json = {}
    statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION)
    statuses.delete(DomainStatus::PENDING_UPDATE)
    statuses.delete(DomainStatus::PENDING_DELETE)
    status_notes[DomainStatus::PENDING_UPDATE] = ''
    status_notes[DomainStatus::PENDING_DELETE] = ''
    save
  end

  def pending_update!
    return true if pending_update?
    self.epp_pending_update = true # for epp

    return true unless registrant_verification_asked?
    pending_json_cache = pending_json
    token = registrant_verification_token
    asked_at = registrant_verification_asked_at
    new_registrant_id    = registrant.id
    new_registrant_email = registrant.email
    new_registrant_name  = registrant.name

    send_time = Time.zone.now + 1.minute
    RegistrantChangeConfirmEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)
    RegistrantChangeNoticeEmailJob.set(wait_until: send_time).perform_later(id, new_registrant_id)

    reload

    self.pending_json = pending_json_cache
    self.registrant_verification_token = token
    self.registrant_verification_asked_at = asked_at
    set_pending_update
    touch_always_version
    pending_json['new_registrant_id']    = new_registrant_id
    pending_json['new_registrant_email'] = new_registrant_email
    pending_json['new_registrant_name']  = new_registrant_name

    # This pending_update! method is triggered by before_update
    # Note, all before_save callbacks are executed before before_update,
    # thus automatic statuses has already executed by this point
    # and we need to trigger automatic statuses manually (second time).
    manage_automatic_statuses
  end

  def registrant_update_confirmable?(token)
    return false if statuses.include? DomainStatus::DELETE_CANDIDATE
    return false unless pending_update?
    return false unless registrant_verification_asked?
    return false unless registrant_verification_token == token
    true
  end

  def registrant_delete_confirmable?(token)
    return false unless pending_delete?
    return false unless registrant_verification_asked?
    return false unless registrant_verification_token == token
    true
  end

  def registrant_verification_asked?
    registrant_verification_asked_at.present? && registrant_verification_token.present?
  end

  def registrant_verification_asked!(frame_str, current_user_id)
    pending_json['frame'] = frame_str
    pending_json['current_user_id'] = current_user_id
    self.registrant_verification_asked_at = Time.zone.now
    self.registrant_verification_token = SecureRandom.hex(42)
  end

  def pending_delete!
    return true if pending_delete?
    self.epp_pending_delete = true # for epp

    # TODO: if this were to ever return true, that would be wrong. EPP would report sucess pending
    return true unless registrant_verification_asked?
    pending_delete_confirmation!
    save(validate: false) # should check if this did succeed

    Domains::DeleteConfirmEmail::SendRequest.run(domain: self)
  end

  def cancel_pending_delete
    statuses.delete DomainStatus::PENDING_DELETE_CONFIRMATION
    statuses.delete DomainStatus::PENDING_DELETE
    self.delete_date = nil
  end

  def pricelist(operation_category, period_i = nil, unit = nil)
    period_i ||= period
    unit ||= period_unit

    zone_name = name.split('.').drop(1).join('.')
    zone = DNS::Zone.find_by(origin: zone_name)

    duration = "P#{period_i}#{unit.upcase}"

    Billing::Price.price_for(zone, operation_category, duration)
  end

  ### VALIDATIONS ###

  def validate_nameserver_ips
    nameservers.to_a.reject(&:marked_for_destruction?).each do |ns|
      next unless ns.hostname.end_with?(".#{name}")
      next if ns.ipv4.present? || ns.ipv6.present?

      errors.add(:nameservers, :invalid) if errors[:nameservers].blank?
      ns.errors.add(:ipv4, :blank)
    end
  end

  # used for highlighting form tabs
  def parent_valid?
    assoc_errors = errors.keys.select { |x| x.match(/\./) }
    (errors.keys - assoc_errors).empty?
  end

  ## SHARED

  def name_in_wire_format
    res = ''
    parts = name_puny.split('.')
    parts.each do |x|
      res += format('%02X', x.length) # length of label in hex
      res += x.each_byte.map { |b| format('%02X', b) }.join # label
    end

    res += '00'

    res
  end

  def to_s
    name
  end

  def pending_registrant
    return '' if pending_json.blank?
    return '' if pending_json['new_registrant_id'].blank?
    Registrant.find_by(id: pending_json['new_registrant_id'])
  end

  def pending_update?
    statuses.include?(DomainStatus::PENDING_UPDATE)
  end

  # depricated not used, not valid
  def update_prohibited?
    (statuses & DomainStatus::UPDATE_PROHIBIT_STATES).present?
  end

  # public api
  def delete_prohibited?
    statuses.include?(DomainStatus::FORCE_DELETE)
  end

  # special handling for admin changing status
  def admin_status_update(update)
    # check for deleted status
    statuses.each do |s|
      unless update.include? s
        case s
          when DomainStatus::PENDING_DELETE
            self.delete_date = nil
          when DomainStatus::SERVER_MANUAL_INZONE # removal causes server hold to set
            self.outzone_at = Time.zone.now if force_delete_scheduled?
          when DomainStatus::EXPIRED # removal causes server hold to set
            self.outzone_at = self.expire_time + 15.day
          when DomainStatus::SERVER_HOLD # removal causes server hold to set
            self.outzone_at = nil
        end
      end
    end
  end

  def pending_update_prohibited?
    (statuses_was & DomainStatus::UPDATE_PROHIBIT_STATES).present?
  end

  def set_pending_update
    if pending_update_prohibited?
      logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_UPDATE not allowed to set. [#{statuses}]"
      return nil
    end
    statuses << DomainStatus::PENDING_UPDATE
  end

  def pending_delete?
    (statuses & [DomainStatus::PENDING_DELETE_CONFIRMATION, DomainStatus::PENDING_DELETE]).any?
  end

  def pending_delete_confirmation?
    statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION
  end

  def pending_delete_confirmation!
    statuses << DomainStatus::PENDING_DELETE_CONFIRMATION unless pending_delete_prohibited?
  end

  def pending_delete_prohibited?
    (statuses_was & DomainStatus::DELETE_PROHIBIT_STATES).present?
  end

  # let's use positive method names
  def pending_deletable?
    !pending_delete_prohibited?
  end

  def set_pending_delete
    if pending_delete_prohibited?
      logger.info "DOMAIN STATUS UPDATE ISSUE ##{id}: PENDING_DELETE not allowed to set. [#{statuses}]"
      return nil
    end
    statuses << DomainStatus::PENDING_DELETE
  end

  def set_server_hold
    statuses << DomainStatus::SERVER_HOLD
    self.outzone_at = Time.current
  end

  def manage_automatic_statuses
    if !self.class.nameserver_required?
      deactivate if nameservers.reject(&:marked_for_destruction?).empty?
      activate if nameservers.reject(&:marked_for_destruction?).size >= Setting.ns_min_count
    end

    cancel_force_delete if force_delete_scheduled? && will_save_change_to_registrant_id?

    if statuses.empty? && valid?
      statuses << DomainStatus::OK
    elsif (statuses.length > 1) || !valid?
      statuses.delete(DomainStatus::OK)
    end

    p_d = statuses.include?(DomainStatus::PENDING_DELETE)
    s_h = (statuses & [DomainStatus::SERVER_MANUAL_INZONE, DomainStatus::SERVER_HOLD]).empty?
    statuses << DomainStatus::SERVER_HOLD if p_d && s_h
  end

  def children_log
    log = HashWithIndifferentAccess.new
    log[:admin_contacts] = admin_contact_ids
    log[:tech_contacts]  = tech_contact_ids
    log[:nameservers]    = nameserver_ids
    log[:dnskeys]        = dnskey_ids
    log[:legal_documents]= [legal_document_id]
    log[:registrant]     = [registrant_id]
    log
  end

  def update_whois_record
    UpdateWhoisRecordJob.set(wait: 1.minute).perform_later name, 'domain'
  end

  def status_notes_array=(notes)
    self.status_notes = {}
    notes ||= []
    statuses.each_with_index do |status, i|
      status_notes[status] = notes[i]
    end
  end

  def primary_contact_emails
    (admin_contacts.emails + [registrant.email]).uniq
  end

  def expired_domain_contact_emails
    (primary_contact_emails +
    ["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
  end

  def all_related_emails
    (admin_contacts.emails +  tech_contacts.emails + [registrant.email]).uniq
  end

  def force_delete_contact_emails
    (primary_contact_emails + tech_contacts.pluck(:email) +
      ["info@#{name}", "#{prepared_domain_name}@#{name}"]).uniq
  end

  def prepared_domain_name
    name.split('.')&.first
  end

  def new_registrant_email
    pending_json['new_registrant_email']
  end

  def new_registrant_id
    pending_json['new_registrant_id']
  end

  def as_json(_options)
    hash = super
    hash['auth_info'] = hash.delete('transfer_code') # API v1 requirement
    hash['valid_from'] = hash['created_at'] # API v1 requirement
    hash.delete('statuses_before_force_delete')
    hash
  end

  def domain_name
    DNS::DomainName.new(name)
  end

  def contact_emails_verification_failed
    contacts.select(&:email_verification_failed?)&.map(&:email)&.uniq
  end

  def self.to_csv
    CSV.generate do |csv|
      csv << column_names
      all.each do |domain|
        csv << domain.attributes.values_at(*column_names)
      end
    end
  end

  def self.pdf(html)
    kit = PDFKit.new(html)
    kit.to_pdf
  end

  def self.expire_warning_period
    Setting.expire_warning_period.days
  end

  def self.redemption_grace_period
    Setting.redemption_grace_period.days
  end

  def self.outzone_candidates
    where("#{attribute_alias(:outzone_time)} < ?", Time.zone.now)
  end

  def self.uses_zone?(zone)
    exists?(["name ILIKE ?", "%.#{zone.origin}"])
  end
end
