# == Schema Information
#
# Table name: dossiers
#
#  id                                                 :integer          not null, primary key
#  api_entreprise_job_exceptions                      :string           is an Array
#  archived                                           :boolean          default(FALSE)
#  autorisation_donnees                               :boolean
#  brouillon_close_to_expiration_notice_sent_at       :datetime
#  conservation_extension                             :interval         default(0 seconds)
#  declarative_triggered_at                           :datetime
#  deleted_user_email_never_send                      :string
#  en_construction_at                                 :datetime
#  en_construction_close_to_expiration_notice_sent_at :datetime
#  en_construction_conservation_extension             :interval         default(0 seconds)
#  en_instruction_at                                  :datetime
#  groupe_instructeur_updated_at                      :datetime
#  hidden_at                                          :datetime
#  last_avis_updated_at                               :datetime
#  last_champ_private_updated_at                      :datetime
#  last_champ_updated_at                              :datetime
#  last_commentaire_updated_at                        :datetime
#  motivation                                         :text
#  private_search_terms                               :text
#  processed_at                                       :datetime
#  search_terms                                       :text
#  state                                              :string
#  termine_close_to_expiration_notice_sent_at         :datetime
#  created_at                                         :datetime
#  updated_at                                         :datetime
#  groupe_instructeur_id                              :bigint
#  revision_id                                        :bigint
#  user_id                                            :integer
#
class Dossier < ApplicationRecord
  self.ignored_columns = [:en_construction_conservation_extension]
  include DossierFilteringConcern

  include Discard::Model
  self.discard_column = :hidden_at
  default_scope -> { kept }

  enum state: {
    brouillon:       'brouillon',
    en_construction: 'en_construction',
    en_instruction:  'en_instruction',
    accepte:         'accepte',
    refuse:          'refuse',
    sans_suite:      'sans_suite'
  }

  EN_CONSTRUCTION_OU_INSTRUCTION = [states.fetch(:en_construction), states.fetch(:en_instruction)]
  TERMINE = [states.fetch(:accepte), states.fetch(:refuse), states.fetch(:sans_suite)]
  INSTRUCTION_COMMENCEE = TERMINE + [states.fetch(:en_instruction)]
  SOUMIS = EN_CONSTRUCTION_OU_INSTRUCTION + TERMINE

  TAILLE_MAX_ZIP = 100.megabytes

  REMAINING_DAYS_BEFORE_CLOSING = 2
  INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
  INTERVAL_BEFORE_EXPIRATION = '1 month'
  INTERVAL_EXPIRATION = '1 month 5 days'

  has_one :etablissement, dependent: :destroy
  has_one :individual, validate: false, dependent: :destroy
  has_one :attestation, dependent: :destroy

  # FIXME: some dossiers have more than one attestation
  has_many :attestations, dependent: :destroy

  has_one_attached :justificatif_motivation

  has_many :champs, -> { root.public_ordered }, inverse_of: false, dependent: :destroy
  has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false, dependent: :destroy
  has_many :commentaires, inverse_of: :dossier, dependent: :destroy
  has_many :invites, dependent: :destroy
  has_many :follows, -> { active }, inverse_of: :dossier
  has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
  has_many :followers_instructeurs, through: :follows, source: :instructeur
  has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
  has_many :avis, inverse_of: :dossier, dependent: :destroy
  has_many :experts, through: :avis
  has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy

  has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier

  belongs_to :groupe_instructeur, optional: true
  belongs_to :revision, class_name: 'ProcedureRevision', optional: false
  belongs_to :user, optional: true
  has_one :france_connect_information, through: :user

  has_one :procedure, through: :revision
  has_many :types_de_champ, through: :revision
  has_many :types_de_champ_private, through: :revision

  accepts_nested_attributes_for :champs
  accepts_nested_attributes_for :champs_private

  include AASM

  aasm whiny_persistence: true, column: :state, enum: true do
    state :brouillon, initial: true
    state :en_construction
    state :en_instruction
    state :accepte
    state :refuse
    state :sans_suite

    event :passer_en_construction, after: :after_passer_en_construction do
      transitions from: :brouillon, to: :en_construction
    end

    event :passer_en_instruction, after: :after_passer_en_instruction do
      transitions from: :en_construction, to: :en_instruction
    end

    event :passer_automatiquement_en_instruction, after: :after_passer_automatiquement_en_instruction do
      transitions from: :en_construction, to: :en_instruction
    end

    event :repasser_en_construction, after: :after_repasser_en_construction do
      transitions from: :en_instruction, to: :en_construction
    end

    event :accepter, after: :after_accepter do
      transitions from: :en_instruction, to: :accepte
    end

    event :accepter_automatiquement, after: :after_accepter_automatiquement do
      transitions from: :en_construction, to: :accepte
    end

    event :refuser, after: :after_refuser do
      transitions from: :en_instruction, to: :refuse
    end

    event :classer_sans_suite, after: :after_classer_sans_suite do
      transitions from: :en_instruction, to: :sans_suite
    end

    event :repasser_en_instruction, after: :after_repasser_en_instruction do
      transitions from: :refuse, to: :en_instruction, guard: :can_repasser_en_instruction?
      transitions from: :sans_suite, to: :en_instruction, guard: :can_repasser_en_instruction?
      transitions from: :accepte, to: :en_instruction, guard: :can_repasser_en_instruction?
    end
  end

  scope :state_brouillon,                      -> { where(state: states.fetch(:brouillon)) }
  scope :state_not_brouillon,                  -> { where.not(state: states.fetch(:brouillon)) }
  scope :state_en_construction,                -> { where(state: states.fetch(:en_construction)) }
  scope :state_en_instruction,                 -> { where(state: states.fetch(:en_instruction)) }
  scope :state_en_construction_ou_instruction, -> { where(state: EN_CONSTRUCTION_OU_INSTRUCTION) }
  scope :state_instruction_commencee,          -> { where(state: INSTRUCTION_COMMENCEE) }
  scope :state_termine,                        -> { where(state: TERMINE) }

  scope :archived,      -> { where(archived: true) }
  scope :not_archived,  -> { where(archived: false) }

  scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
  scope :order_by_created_at, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
  scope :updated_since,       -> (since) { where('dossiers.updated_at >= ?', since) }
  scope :created_since,       -> (since) { where('dossiers.en_construction_at >= ?', since) }

  scope :with_type_de_champ, -> (stable_id) {
    joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
      .where('types_de_champ.private = FALSE AND types_de_champ.stable_id = ?', stable_id)
  }

  scope :with_type_de_champ_private, -> (stable_id) {
    joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
      .where('types_de_champ.private = TRUE AND types_de_champ.stable_id = ?', stable_id)
  }

  scope :all_state,                   -> { not_archived.state_not_brouillon }
  scope :en_construction,             -> { not_archived.state_en_construction }
  scope :en_instruction,              -> { not_archived.state_en_instruction }
  scope :termine,                     -> { not_archived.state_termine }
  scope :processed_in_month, -> (month) do
    state_termine
      .joins(:traitements)
      .where(traitements: { processed_at: month.beginning_of_month..month.end_of_month })
  end
  scope :downloadable_sorted, -> {
    state_not_brouillon
      .includes(
        :user,
        :individual,
        :followers_instructeurs,
        :traitements,
        :groupe_instructeur,
        procedure: [
          :groupe_instructeurs,
          :draft_types_de_champ,
          :draft_types_de_champ_private,
          :published_types_de_champ,
          :published_types_de_champ_private
        ],
        avis: [:claimant, :expert],
        etablissement: :champ,
        champs: {
          type_de_champ: [],
          etablissement: :champ,
          piece_justificative_file_attachment: :blob,
          champs: [
            type_de_champ: [],
            piece_justificative_file_attachment: :blob
          ]
        },
        champs_private: {
          type_de_champ: [],
          etablissement: :champ,
          piece_justificative_file_attachment: :blob,
          champs: [
            type_de_champ: [],
            piece_justificative_file_attachment: :blob
          ]
        }
      ).order(en_construction_at: 'asc')
  }
  scope :en_cours,                    -> { not_archived.state_en_construction_ou_instruction }
  scope :without_followers,           -> { left_outer_joins(:follows).where(follows: { id: nil }) }
  scope :with_champs,                 -> { includes(champs: :type_de_champ) }
  scope :for_api, -> {
    includes(commentaires: { piece_jointe_attachment: :blob },
      champs: [
        :geo_areas,
        :etablissement,
        piece_justificative_file_attachment: :blob,
        champs: [
          piece_justificative_file_attachment: :blob
        ]
      ],
      champs_private: [
        :geo_areas,
        :etablissement,
        piece_justificative_file_attachment: :blob,
        champs: [
          piece_justificative_file_attachment: :blob
        ]
      ],
      justificatif_motivation_attachment: :blob,
      attestation: [],
      avis: { piece_justificative_file_attachment: :blob },
      traitements: [],
      etablissement: [],
      individual: [],
      user: [])
  }

  scope :with_notifiable_procedure, -> (opts = { notify_on_closed: false }) do
    states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee]
    joins(:procedure)
      .where(procedures: { aasm_state: states })
      .where.not(user_id: nil)
  end

  scope :brouillon_close_to_expiration, -> do
    state_brouillon
      .joins(:procedure)
      .where("dossiers.created_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
  end
  scope :en_construction_close_to_expiration, -> do
    state_en_construction
      .joins(:procedure)
      .where("dossiers.en_construction_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
  end
  scope :en_instruction_close_to_expiration, -> do
    state_en_instruction
      .joins(:procedure)
      .where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
  end
  scope :termine_close_to_expiration, -> do
    state_termine
      .joins(:procedure)
      .where(id: Traitement.termine_close_to_expiration.pluck(:dossier_id).uniq)
  end

  scope :brouillon_expired, -> do
    state_brouillon
      .where("brouillon_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
  end
  scope :en_construction_expired, -> do
    state_en_construction
      .where("en_construction_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
  end
  scope :termine_expired, -> do
    state_termine
      .where("termine_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
  end

  scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
  scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) }
  scope :without_termine_expiration_notice_sent, -> { where(termine_close_to_expiration_notice_sent_at: nil) }

  scope :discarded_brouillon_expired, -> do
    with_discarded
      .discarded
      .state_brouillon
      .where('hidden_at < ?', 1.week.ago)
  end
  scope :discarded_en_construction_expired, -> do
    with_discarded
      .discarded
      .state_en_construction
      .where('dossiers.hidden_at < ?', 1.week.ago)
  end
  scope :discarded_termine_expired, -> do
    with_discarded
      .discarded
      .state_termine
      .where('dossiers.hidden_at < ?', 1.week.ago)
  end

  scope :brouillon_near_procedure_closing_date, -> do
    # select users who have submitted dossier for the given 'procedures.id'
    users_who_submitted =
      state_not_brouillon
        .joins(:revision)
        .where("procedure_revisions.procedure_id = procedures.id")
        .select(:user_id)
    # select dossier in brouillon where procedure closes in two days and for which the user has not submitted a Dossier
    state_brouillon
      .with_notifiable_procedure
      .where("procedures.auto_archive_on - INTERVAL :before_closing = :now", { now: Time.zone.today, before_closing: INTERVAL_BEFORE_CLOSING })
      .where.not(user: users_who_submitted)
  end

  scope :for_api_v2, -> { includes(procedure: [:administrateurs, :attestation_template], etablissement: [], individual: [], traitements: []) }

  scope :with_notifications, -> do
    joins(:follows)
      .where('last_champ_updated_at > follows.demande_seen_at' \
      ' OR groupe_instructeur_updated_at > follows.demande_seen_at' \
      ' OR last_champ_private_updated_at > follows.annotations_privees_seen_at' \
      ' OR last_avis_updated_at > follows.avis_seen_at' \
      ' OR last_commentaire_updated_at > follows.messagerie_seen_at')
      .distinct
  end

  accepts_nested_attributes_for :individual

  delegate :siret, :siren, to: :etablissement, allow_nil: true
  delegate :france_connect_information, to: :user, allow_nil: true

  before_save :build_default_champs, if: Proc.new { revision_id_was.nil? }
  before_save :update_search_terms

  after_save :send_dossier_en_instruction
  after_save :send_web_hook
  after_create_commit :send_draft_notification_email

  validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }
  validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
  validates :groupe_instructeur, presence: true, if: -> { !brouillon? }

  def user_deleted?
    user_id.nil?
  end

  def user_email_for(use)
    if user_deleted?
      if use == :display
        deleted_user_email_never_send
      else
        raise "Can not send email to discarded user"
      end
    else
      user.email
    end
  end

  def motivation
    return nil if !termine?
    traitements.any? ? traitements.last.motivation : read_attribute(:motivation)
  end

  def processed_at
    return nil if !termine?
    traitements.any? ? traitements.last.processed_at : read_attribute(:processed_at)
  end

  def update_search_terms
    self.search_terms = [
      user&.email,
      *champs.flat_map(&:search_terms),
      *etablissement&.search_terms,
      individual&.nom,
      individual&.prenom
    ].compact.join(' ')
    self.private_search_terms = champs_private.flat_map(&:search_terms).compact.join(' ')
  end

  def build_default_champs
    revision.build_champs.each do |champ|
      champs << champ
    end
    revision.build_champs_private.each do |champ|
      champs_private << champ
    end
  end

  def build_default_individual
    if procedure.for_individual? && individual.blank?
      self.individual = if france_connect_information.present?
        Individual.from_france_connect(france_connect_information)
      else
        Individual.new
      end
    end
  end

  def en_construction_ou_instruction?
    EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
  end

  def termine?
    TERMINE.include?(state)
  end

  def instruction_commencee?
    INSTRUCTION_COMMENCEE.include?(state)
  end

  def reset!
    etablissement.destroy

    update_columns(autorisation_donnees: false)
  end

  def read_only?
    en_instruction? || accepte? || refuse? || sans_suite?
  end

  def can_transition_to_en_construction?
    brouillon? && procedure.dossier_can_transition_to_en_construction?
  end

  def can_repasser_en_instruction?
    termine? && !user_deleted?
  end

  def can_be_updated_by_user?
    brouillon? || en_construction?
  end

  def can_be_deleted_by_user?
    brouillon? || en_construction?
  end

  def can_be_deleted_by_manager?
    kept? && can_be_deleted_by_user?
  end

  def messagerie_available?
    !brouillon? && !user_deleted? && !archived
  end

  def en_construction_close_to_expiration?
    self.class.en_construction_close_to_expiration.exists?(id: self)
  end

  def brouillon_close_to_expiration?
    self.class.brouillon_close_to_expiration.exists?(id: self)
  end

  def close_to_expiration?
    en_construction_close_to_expiration? || brouillon_close_to_expiration?
  end

  def show_groupe_instructeur_details?
    procedure.routee? && groupe_instructeur.present? && (!procedure.feature_enabled?(:procedure_routage_api) || !defaut_groupe_instructeur?)
  end

  def show_groupe_instructeur_selector?
    procedure.routee? && !procedure.feature_enabled?(:procedure_routage_api)
  end

  def assign_to_groupe_instructeur(groupe_instructeur, author = nil)
    if (groupe_instructeur.nil? || groupe_instructeur.procedure == procedure) && self.groupe_instructeur != groupe_instructeur
      if update(groupe_instructeur: groupe_instructeur, groupe_instructeur_updated_at: Time.zone.now)
        unfollow_stale_instructeurs

        if author.present?
          log_dossier_operation(author, :changer_groupe_instructeur, self)
        end

        true
      end
    else
      false
    end
  end

  def archiver!(author)
    update!(archived: true)
    log_dossier_operation(author, :archiver)
  end

  def desarchiver!(author)
    update!(archived: false)
    log_dossier_operation(author, :desarchiver)
  end

  def text_summary
    if brouillon?
      parts = [
        "Dossier en brouillon répondant à la démarche ",
        procedure.libelle,
        " gérée par l'organisme ",
        procedure.organisation_name
      ]
    else
      parts = [
        "Dossier déposé le ",
        en_construction_at.strftime("%d/%m/%Y"),
        " sur la démarche ",
        procedure.libelle,
        " gérée par l'organisme ",
        procedure.organisation_name
      ]
    end

    parts.join
  end

  def avis_for_instructeur(instructeur)
    if instructeur.dossiers.include?(self)
      avis.order(created_at: :asc)
    else
      avis
        .where(confidentiel: false)
        .or(avis.where(claimant: instructeur))
        .order(created_at: :asc)
    end
  end

  def avis_for_expert(expert)
    Avis
      .where(dossier_id: id, confidentiel: false)
      .or(Avis.where(id: expert.avis, dossier_id: id))
      .order(created_at: :asc)
  end

  def owner_name
    if etablissement.present?
      etablissement.entreprise_raison_sociale
    elsif individual.present?
      "#{individual.nom} #{individual.prenom}"
    end
  end

  def log_operations?
    !procedure.brouillon?
  end

  def keep_track_on_deletion?
    !procedure.brouillon?
  end

  def expose_legacy_carto_api?
    procedure.expose_legacy_carto_api?
  end

  def geo_position
    if etablissement.present?
      point = Geocoder.search(etablissement.geo_adresse).first
    end

    lon = Champs::CarteChamp::DEFAULT_LON.to_s
    lat = Champs::CarteChamp::DEFAULT_LAT.to_s
    zoom = "13"

    if point.present?
      lat, lon = point.coordinates.map(&:to_s)
    end

    { lon: lon, lat: lat, zoom: zoom }
  end

  def unspecified_attestation_champs
    attestation_template = procedure.attestation_template

    if attestation_template&.activated?
      attestation_template.unspecified_champs_for_dossier(self)
    else
      []
    end
  end

  def build_attestation
    if procedure.attestation_template&.activated?
      procedure.attestation_template.attestation_for(self)
    end
  end

  def expired_keep_track!
    if keep_track_on_deletion?
      DeletedDossier.create_from_dossier(self, :expired)
      log_automatic_dossier_operation(:supprimer, self)
    end
  end

  def discard_and_keep_track!(author, reason)
    if keep_track_on_deletion?
      if en_construction?
        deleted_dossier = DeletedDossier.create_from_dossier(self, reason)

        administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
        administration_emails.each do |email|
          DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
        end

        if !user_deleted?
          DossierMailer.notify_deletion_to_user(deleted_dossier, user_email_for(:notification)).deliver_later
        end

        log_dossier_operation(author, :supprimer, self)
      elsif termine?
        deleted_dossier = DeletedDossier.create_from_dossier(self, reason)

        if !user_deleted?
          DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, user_email_for(:notification)).deliver_later
        end

        log_dossier_operation(author, :supprimer, self)
      end
    end

    discard!
  end

  def restore(author, only_discarded_with_procedure = false)
    if discarded?
      deleted_dossier = DeletedDossier.find_by(dossier_id: id)

      if !only_discarded_with_procedure || deleted_dossier&.procedure_removed?
        if undiscard && keep_track_on_deletion? && en_construction?
          deleted_dossier&.destroy
          log_dossier_operation(author, :restaurer, self)
        end
      end
    end
  end

  def after_passer_en_construction
    update!(conservation_extension: 0.days)
    update!(en_construction_at: Time.zone.now) if self.en_construction_at.nil?
  end

  def after_passer_en_instruction(instructeur)
    instructeur.follow(self)

    update!(en_instruction_at: Time.zone.now) if self.en_instruction_at.nil?
    log_dossier_operation(instructeur, :passer_en_instruction)
  end

  def after_passer_automatiquement_en_instruction
    self.en_instruction_at ||= Time.zone.now
    self.declarative_triggered_at = Time.zone.now
    save!
    log_automatic_dossier_operation(:passer_en_instruction)
  end

  def after_repasser_en_construction(instructeur)
    update!(conservation_extension: 0.days)
    log_dossier_operation(instructeur, :repasser_en_construction)
  end

  def after_repasser_en_instruction(instructeur)
    self.archived = false
    self.en_instruction_at = Time.zone.now
    attestation&.destroy

    save!
    DossierMailer.notify_revert_to_instruction(self).deliver_later
    log_dossier_operation(instructeur, :repasser_en_instruction)
  end

  def after_accepter(instructeur, motivation, justificatif = nil)
    self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)

    if justificatif
      self.justificatif_motivation.attach(justificatif)
    end

    if attestation.nil?
      self.attestation = build_attestation
    end

    save!
    remove_titres_identite!
    NotificationMailer.send_accepte_notification(self).deliver_later
    send_dossier_decision_to_experts(self)
    log_dossier_operation(instructeur, :accepter, self)
  end

  def after_accepter_automatiquement
    self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: nil, motivation: nil, processed_at: Time.zone.now)
    self.en_instruction_at ||= Time.zone.now
    self.declarative_triggered_at = Time.zone.now

    if attestation.nil?
      self.attestation = build_attestation
    end

    save!
    remove_titres_identite!
    NotificationMailer.send_accepte_notification(self).deliver_later
    log_automatic_dossier_operation(:accepter, self)
  end

  def after_refuser(instructeur, motivation, justificatif = nil)
    self.traitements.build(state: Dossier.states.fetch(:refuse), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)

    if justificatif
      self.justificatif_motivation.attach(justificatif)
    end

    save!
    remove_titres_identite!
    NotificationMailer.send_refuse_notification(self).deliver_later
    send_dossier_decision_to_experts(self)
    log_dossier_operation(instructeur, :refuser, self)
  end

  def after_classer_sans_suite(instructeur, motivation, justificatif = nil)
    self.traitements.build(state: Dossier.states.fetch(:sans_suite), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)

    if justificatif
      self.justificatif_motivation.attach(justificatif)
    end

    save!
    remove_titres_identite!
    NotificationMailer.send_sans_suite_notification(self).deliver_later
    send_dossier_decision_to_experts(self)
    log_dossier_operation(instructeur, :classer_sans_suite, self)
  end

  def remove_titres_identite!
    champs.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
  end

  def check_mandatory_champs
    (champs + champs.filter(&:repetition?).flat_map(&:champs))
      .filter(&:mandatory_and_blank?)
      .map do |champ|
        "Le champ #{champ.libelle.truncate(200)} doit être rempli."
      end
  end

  def log_modifier_annotations!(instructeur)
    champs_private.filter(&:value_previously_changed?).each do |champ|
      log_dossier_operation(instructeur, :modifier_annotation, champ)
    end
  end

  def log_modifier_annotation!(champ, instructeur)
    log_dossier_operation(instructeur, :modifier_annotation, champ)
  end

  def demander_un_avis!(avis)
    log_dossier_operation(avis.claimant, :demander_un_avis, avis)
  end

  def spreadsheet_columns_csv(types_de_champ:)
    spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
  end

  def spreadsheet_columns_xlsx(types_de_champ:)
    spreadsheet_columns(types_de_champ: types_de_champ)
  end

  def spreadsheet_columns_ods(types_de_champ:)
    spreadsheet_columns(types_de_champ: types_de_champ)
  end

  def spreadsheet_columns(with_etablissement: false, types_de_champ:)
    columns = [
      ['ID', id.to_s],
      ['Email', user_email_for(:display)]
    ]

    if procedure.for_individual?
      columns += [
        ['Civilité', individual&.gender],
        ['Nom', individual&.nom],
        ['Prénom', individual&.prenom]
      ]
      if procedure.ask_birthday
        columns += [['Date de naissance', individual&.birthdate]]
      end
    elsif with_etablissement
      columns += [
        ['Établissement SIRET', etablissement&.siret],
        ['Établissement siège social', etablissement&.siege_social],
        ['Établissement NAF', etablissement&.naf],
        ['Établissement libellé NAF', etablissement&.libelle_naf],
        ['Établissement Adresse', etablissement&.adresse],
        ['Établissement numero voie', etablissement&.numero_voie],
        ['Établissement type voie', etablissement&.type_voie],
        ['Établissement nom voie', etablissement&.nom_voie],
        ['Établissement complément adresse', etablissement&.complement_adresse],
        ['Établissement code postal', etablissement&.code_postal],
        ['Établissement localité', etablissement&.localite],
        ['Établissement code INSEE localité', etablissement&.code_insee_localite],
        ['Entreprise SIREN', etablissement&.entreprise_siren],
        ['Entreprise capital social', etablissement&.entreprise_capital_social],
        ['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
        ['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
        ['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
        ['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
        ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
        ['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
        ['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
        ['Entreprise date de création', etablissement&.entreprise_date_creation],
        ['Entreprise nom', etablissement&.entreprise_nom],
        ['Entreprise prénom', etablissement&.entreprise_prenom],
        ['Association RNA', etablissement&.association_rna],
        ['Association titre', etablissement&.association_titre],
        ['Association objet', etablissement&.association_objet],
        ['Association date de création', etablissement&.association_date_creation],
        ['Association date de déclaration', etablissement&.association_date_declaration],
        ['Association date de publication', etablissement&.association_date_publication]
      ]
    else
      columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
    end

    columns += [
      ['Archivé', :archived],
      ['État du dossier', Dossier.human_attribute_name("state.#{state}")],
      ['Dernière mise à jour le', :updated_at],
      ['Déposé le', :en_construction_at],
      ['Passé en instruction le', :en_instruction_at],
      ['Traité le', :processed_at],
      ['Motivation de la décision', :motivation],
      ['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
    ]

    if procedure.routee?
      columns << ['Groupe instructeur', groupe_instructeur.label]
    end

    columns + champs_for_export(types_de_champ)
  end

  def champs_for_export(types_de_champ)
    # Index values by stable_id
    values = (champs + champs_private).reject(&:exclude_from_export?).reduce({}) do |champs, champ|
      champs[champ.stable_id] = champ.for_export
      champs
    end

    # Get all the champs values for the types de champ in the final list.
    # Dossier might not have corresponding champ – display nil.
    types_de_champ.map do |type_de_champ|
      [type_de_champ.libelle, values[type_de_champ.stable_id]]
    end
  end

  def export_and_attachments_downloadable?
    PiecesJustificativesService.pieces_justificatives_total_size(self) < Dossier::TAILLE_MAX_ZIP
  end

  def linked_dossiers_for(instructeur_or_expert)
    dossier_ids = champs.filter(&:dossier_link?).filter_map(&:value)
    instructeur_or_expert.dossiers.where(id: dossier_ids)
  end

  def hash_for_deletion_mail
    { id: self.id, procedure_libelle: self.procedure.libelle }
  end

  def geo_data?
    geo_areas.present?
  end

  def to_feature_collection
    {
      type: 'FeatureCollection',
      id: id,
      bbox: bounding_box,
      features: geo_areas.map(&:to_feature)
    }
  end

  def log_api_entreprise_job_exception(exception)
    exceptions = self.api_entreprise_job_exceptions ||= []
    exceptions << exception.inspect
    update_column(:api_entreprise_job_exceptions, exceptions)
  end

  private

  def defaut_groupe_instructeur?
    groupe_instructeur == procedure.defaut_groupe_instructeur
  end

  def geo_areas
    champs.includes(:geo_areas).flat_map(&:geo_areas) + champs_private.includes(:geo_areas).flat_map(&:geo_areas)
  end

  def bounding_box
    factory = RGeo::Geographic.simple_mercator_factory
    bounding_box = RGeo::Cartesian::BoundingBox.new(factory)

    geo_areas.filter_map(&:rgeo_geometry).each do |geometry|
      bounding_box.add(geometry)
    end

    [bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates)
  end

  def log_dossier_operation(author, operation, subject = nil)
    if log_operations?
      DossierOperationLog.create_and_serialize(
        dossier: self,
        operation: DossierOperationLog.operations.fetch(operation),
        author: author,
        subject: subject
      )
    end
  end

  def log_automatic_dossier_operation(operation, subject = nil)
    if log_operations?
      DossierOperationLog.create_and_serialize(
        dossier: self,
        operation: DossierOperationLog.operations.fetch(operation),
        automatic_operation: true,
        subject: subject
      )
    end
  end

  def send_dossier_en_instruction
    if saved_change_to_state? && en_instruction? && !procedure.declarative_accepte?
      NotificationMailer.send_en_instruction_notification(self).deliver_later
    end
  end

  def send_draft_notification_email
    if brouillon? && !procedure.declarative?
      DossierMailer.notify_new_draft(self).deliver_later
    end
  end

  def send_web_hook
    if saved_change_to_state? && !brouillon? && procedure.web_hook_url.present?
      WebHookJob.perform_later(
        procedure,
        self
      )
    end
  end

  def unfollow_stale_instructeurs
    followers_instructeurs.each do |instructeur|
      if instructeur.groupe_instructeurs.exclude?(groupe_instructeur)
        instructeur.unfollow(self)
        if kept?
          DossierMailer.notify_groupe_instructeur_changed(instructeur, self).deliver_later
        end
      end
    end
  end

  def self.notify_draft_not_submitted
    brouillon_near_procedure_closing_date
      .includes(:user)
      .find_each do |dossier|
        DossierMailer.notify_brouillon_not_submitted(dossier).deliver_later
      end
  end

  def send_dossier_decision_to_experts(dossier)
    avis_experts_procedures_ids = Avis
      .joins(:experts_procedure)
      .where(dossier: dossier, experts_procedures: { allow_decision_access: true })
      .with_answer
      .distinct
      .pluck('avis.id, experts_procedures.id')

    # rubocop:disable Lint/UnusedBlockArgument
    avis_ids = avis_experts_procedures_ids
      .uniq { |(avis_id, experts_procedures_id)| experts_procedures_id }
      .map { |(avis_id, _)| avis_id }
    # rubocop:enable Lint/UnusedBlockArgument

    avis_ids.each { |avis_id| ExpertMailer.send_dossier_decision(avis_id).deliver_later }
  end
end