# == Schema Information
#
# Table name: procedures
#
#  id                                        :integer          not null, primary key
#  aasm_state                                :string           default("brouillon")
#  allow_expert_review                       :boolean          default(TRUE), not null
#  api_entreprise_token                      :string
#  api_particulier_scopes                    :text             default([]), is an Array
#  api_particulier_sources                   :jsonb
#  ask_birthday                              :boolean          default(FALSE), not null
#  auto_archive_on                           :date
#  cadre_juridique                           :string
#  cerfa_flag                                :boolean          default(FALSE)
#  cloned_from_library                       :boolean          default(FALSE)
#  closed_at                                 :datetime
#  declarative_with_state                    :string
#  description                               :string
#  direction                                 :string
#  duree_conservation_dossiers_dans_ds       :integer
#  duree_conservation_dossiers_hors_ds       :integer
#  durees_conservation_required              :boolean          default(TRUE)
#  encrypted_api_particulier_token           :string
#  euro_flag                                 :boolean          default(FALSE)
#  experts_require_administrateur_invitation :boolean          default(FALSE)
#  for_individual                            :boolean          default(FALSE)
#  hidden_at                                 :datetime
#  instructeurs_self_management_enabled      :boolean
#  juridique_required                        :boolean          default(TRUE)
#  libelle                                   :string
#  lien_demarche                             :string
#  lien_notice                               :string
#  lien_site_web                             :string
#  monavis_embed                             :text
#  organisation                              :string
#  path                                      :string           not null
#  procedure_expires_when_termine_enabled    :boolean          default(FALSE)
#  published_at                              :datetime
#  routing_criteria_name                     :text             default("Votre ville")
#  routing_enabled                           :boolean
#  test_started_at                           :datetime
#  unpublished_at                            :datetime
#  web_hook_url                              :string
#  whitelisted_at                            :datetime
#  created_at                                :datetime         not null
#  updated_at                                :datetime         not null
#  canonical_procedure_id                    :bigint
#  draft_revision_id                         :bigint
#  parent_procedure_id                       :bigint
#  published_revision_id                     :bigint
#  service_id                                :bigint
#  zone_id                                   :bigint
#

class Procedure < ApplicationRecord
  self.ignored_columns = [:duree_conservation_dossiers_hors_ds]
  include ProcedureStatsConcern
  include EncryptableConcern

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

  MAX_DUREE_CONSERVATION = 36
  MAX_DUREE_CONSERVATION_EXPORT = 3.hours

  MIN_WEIGHT = 350000

  attr_encrypted :api_particulier_token

  has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure
  belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false
  belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true
  has_many :deleted_dossiers, dependent: :destroy

  has_many :published_types_de_champ, through: :published_revision, source: :types_de_champ
  has_many :published_types_de_champ_private, through: :published_revision, source: :types_de_champ_private
  has_many :draft_types_de_champ, through: :draft_revision, source: :types_de_champ
  has_many :draft_types_de_champ_private, through: :draft_revision, source: :types_de_champ_private

  has_many :experts_procedures, dependent: :destroy
  has_many :experts, through: :experts_procedures

  has_one :module_api_carto, dependent: :destroy
  has_one :attestation_template, dependent: :destroy

  belongs_to :parent_procedure, class_name: 'Procedure', optional: true
  belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
  belongs_to :service, optional: true
  belongs_to :zone, optional: true

  def active_revision
    brouillon? ? draft_revision : published_revision
  end

  def types_de_champ
    brouillon? ? draft_types_de_champ : published_types_de_champ
  end

  def types_de_champ_private
    brouillon? ? draft_types_de_champ_private : published_types_de_champ_private
  end

  def types_de_champ_for_procedure_presentation
    if brouillon?
      TypeDeChamp.fillable
        .joins(:revision_types_de_champ)
        .where(revision_types_de_champ: { revision: draft_revision, parent_id: nil })
        .order(:private, :position)
    else
      # fetch all type_de_champ.stable_id for all the revisions expect draft
      # and for each stable_id take the bigger (more recent) type_de_champ.id
      recent_ids = TypeDeChamp.fillable
        .joins(:revisions)
        .where(procedure_revisions: { procedure_id: id })
        .where.not(procedure_revisions: { id: draft_revision_id })
        .where(revision_types_de_champ: { parent_id: nil })
        .group(:stable_id)
        .select('MAX(types_de_champ.id)')

      # fetch the more recent procedure_revision_types_de_champ
      # which includes recents_ids
      recents_prtdc = ProcedureRevisionTypeDeChamp
        .root
        .where(type_de_champ_id: recent_ids)
        .where.not(revision_id: draft_revision_id)
        .group(:type_de_champ_id)
        .select('MAX(id)')

      TypeDeChamp
        .joins(:revision_types_de_champ)
        .where(revision_types_de_champ: { id: recents_prtdc })
        .order(:private, :position, 'revision_types_de_champ.revision_id': :desc)
    end
  end

  def types_de_champ_for_tags
    if brouillon?
      draft_types_de_champ
    else
      TypeDeChamp.root
        .public_only
        .fillable
        .joins(:revisions)
        .where(procedure_revisions: { procedure_id: id })
        .where.not(procedure_revisions: { id: draft_revision_id })
        .where(revision_types_de_champ: { parent_id: nil })
        .order(:created_at)
        .uniq
    end
  end

  def types_de_champ_private_for_tags
    if brouillon?
      draft_types_de_champ_private
    else
      TypeDeChamp.root
        .private_only
        .fillable
        .joins(:revisions)
        .where(procedure_revisions: { procedure_id: id })
        .where.not(procedure_revisions: { id: draft_revision_id })
        .where(revision_types_de_champ: { parent_id: nil })
        .order(:created_at)
        .uniq
    end
  end

  has_many :administrateurs_procedures
  has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! }
  has_many :groupe_instructeurs, dependent: :destroy
  has_many :instructeurs, through: :groupe_instructeurs

  # This relationship is used in following dossiers through. We can not use revisions relationship
  # as order scope introduces invalid sql in some combinations.
  has_many :unordered_revisions, class_name: 'ProcedureRevision', inverse_of: :procedure, dependent: :destroy
  has_many :dossiers, through: :unordered_revisions, dependent: :restrict_with_exception

  has_one :initiated_mail, class_name: "Mails::InitiatedMail", dependent: :destroy
  has_one :received_mail, class_name: "Mails::ReceivedMail", dependent: :destroy
  has_one :closed_mail, class_name: "Mails::ClosedMail", dependent: :destroy
  has_one :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy
  has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", dependent: :destroy

  has_one :defaut_groupe_instructeur, -> { order(:label) }, class_name: 'GroupeInstructeur', inverse_of: :procedure

  has_one_attached :logo
  has_one_attached :notice
  has_one_attached :deliberation

  scope :brouillons,            -> { where(aasm_state: :brouillon) }
  scope :publiees,              -> { where(aasm_state: :publiee) }
  scope :closes,                -> { where(aasm_state: [:close, :depubliee]) }
  scope :publiees_ou_closes,    -> { where(aasm_state: [:publiee, :close, :depubliee]) }
  scope :by_libelle,            -> { order(libelle: :asc) }
  scope :created_during,        -> (range) { where(created_at: range) }
  scope :cloned_from_library,   -> { where(cloned_from_library: true) }
  scope :declarative,           -> { where.not(declarative_with_state: nil) }

  scope :discarded_expired, -> do
    with_discarded
      .discarded
      .where('hidden_at < ?', 1.month.ago)
  end

  scope :for_api, -> {
    includes(
      :administrateurs,
      :module_api_carto,
      published_revision: [
        :types_de_champ_private,
        :types_de_champ
      ],
      draft_revision: [
        :types_de_champ_private,
        :types_de_champ
      ]
    )
  }

  enum declarative_with_state: {
    en_instruction:  'en_instruction',
    accepte:         'accepte'
  }

  scope :for_api_v2, -> {
    includes(:draft_revision, :published_revision, administrateurs: :user)
  }

  scope :for_download, -> {
    includes(
      :groupe_instructeurs,
      dossiers: {
        champs: [
          piece_justificative_file_attachment: :blob,
          champs: [
            piece_justificative_file_attachment: :blob
          ]
        ]
      }
    )
  }

  validates :libelle, presence: true, allow_blank: false, allow_nil: false
  validates :description, presence: true, allow_blank: false, allow_nil: false
  validates :administrateurs, presence: true
  validates :lien_site_web, presence: true, if: :publiee?
  validates :draft_revision,
    'revisions/no_empty_repetition': true,
    'revisions/no_empty_drop_down': true,
    if: :validate_for_publication?
  validate :check_juridique
  validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
  validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }
  validates_with MonAvisEmbedValidator

  FILE_MAX_SIZE = 20.megabytes
  validates :notice, content_type: [
    "application/msword",
    "application/pdf",
    "application/vnd.ms-powerpoint",
    "application/vnd.oasis.opendocument.presentation",
    "application/vnd.oasis.opendocument.text",
    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "image/jpeg",
    "image/jpg",
    "image/png",
    "text/plain"
  ], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }

  validates :deliberation, content_type: [
    "application/msword",
    "application/pdf",
    "application/vnd.oasis.opendocument.text",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "image/jpeg",
    "image/jpg",
    "image/png",
    "text/plain"
  ], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 4, 29) }

  LOGO_MAX_SIZE = 5.megabytes
  validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
    size: { less_than: LOGO_MAX_SIZE },
    if: -> { new_record? || created_at > Date.new(2020, 11, 13) }

  validates :api_entreprise_token, jwt_token: true, allow_blank: true
  validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/ }, allow_blank: true

  before_save :update_juridique_required
  after_initialize :ensure_path_exists
  before_save :ensure_path_exists
  after_create :ensure_defaut_groupe_instructeur

  include AASM

  aasm whiny_persistence: true do
    state :brouillon, initial: true
    state :publiee
    state :close
    state :depubliee

    event :publish, before: :before_publish do
      transitions from: :brouillon, to: :publiee, after: :after_publish
      transitions from: :close, to: :publiee, after: :after_republish
      transitions from: :depubliee, to: :publiee, after: :after_republish
    end

    event :close, after: :after_close do
      transitions from: :publiee, to: :close
    end

    event :unpublish, after: :after_unpublish do
      transitions from: :publiee, to: :depubliee
    end
  end

  def publish_or_reopen!(administrateur)
    Procedure.transaction do
      if brouillon?
        reset!
      end

      other_procedure = other_procedure_with_path(path)
      if other_procedure.present? && administrateur.owns?(other_procedure)
        other_procedure.unpublish!
        publish!(other_procedure.canonical_procedure || other_procedure)
      else
        publish!
      end
    end
  end

  def reset!
    if !locked? || draft_changed?
      draft_revision.dossiers.destroy_all
    end
  end

  def suggested_path(administrateur)
    if path_customized?
      return path
    end
    slug = libelle&.parameterize&.first(50)
    suggestion = slug
    counter = 1
    while !path_available?(administrateur, suggestion)
      counter = counter + 1
      suggestion = "#{slug}-#{counter}"
    end
    suggestion
  end

  def other_procedure_with_path(path)
    Procedure.publiees
      .where.not(id: self.id)
      .find_by(path: path)
  end

  def path_available?(administrateur, path)
    procedure = other_procedure_with_path(path)

    procedure.blank? || (administrateur.owns?(procedure) && canonical_procedure_child?(procedure))
  end

  def canonical_procedure_child?(procedure)
    !canonical_procedure || canonical_procedure == procedure || canonical_procedure == procedure.canonical_procedure
  end

  def locked?
    publiee? || close? || depubliee?
  end

  def draft_changed?
    publiee? && published_revision.changed?(draft_revision) && revision_changes.present?
  end

  def revision_changes
    published_revision.compare(draft_revision)
  end

  def accepts_new_dossiers?
    publiee? || brouillon?
  end

  def dossier_can_transition_to_en_construction?
    accepts_new_dossiers? || depubliee?
  end

  def expose_legacy_carto_api?
    module_api_carto&.use_api_carto? && module_api_carto&.migrated?
  end

  def declarative?
    declarative_with_state.present?
  end

  def declarative_accepte?
    declarative_with_state == Procedure.declarative_with_states.fetch(:accepte)
  end

  def self.declarative_attributes_for_select
    declarative_with_states.map do |state, _|
      [I18n.t("activerecord.attributes.#{model_name.i18n_key}.declarative_with_state/#{state}"), state]
    end
  end

  def feature_enabled?(feature)
    Flipper.enabled?(feature, self)
  end

  def path_customized?
    !path.match?(/[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}/)
  end

  def organisation_name
    service&.nom || organisation
  end

  def self.active(id)
    publiees.find(id)
  end

  def clone(admin, from_library)
    is_different_admin = !admin.owns?(self)

    populate_champ_stable_ids
    include_list = {
      attestation_template: [],
      draft_revision: {
        revision_types_de_champ: {
          type_de_champ: :types_de_champ
        },
        revision_types_de_champ_private: {
          type_de_champ: :types_de_champ
        }
      }
    }
    include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin
    procedure = self.deep_clone(include: include_list) do |original, kopy|
      PiecesJustificativesService.clone_attachments(original, kopy)
    end
    procedure.path = SecureRandom.uuid
    procedure.aasm_state = :brouillon
    procedure.closed_at = nil
    procedure.unpublished_at = nil
    procedure.published_at = nil
    procedure.lien_notice = nil
    procedure.published_revision = nil
    procedure.draft_revision.procedure = procedure

    if is_different_admin
      procedure.administrateurs = [admin]
      procedure.api_entreprise_token = nil
      procedure.encrypted_api_particulier_token = nil
      procedure.api_particulier_scopes = []
    else
      procedure.administrateurs = administrateurs
    end

    procedure.initiated_mail = initiated_mail&.dup
    procedure.received_mail = received_mail&.dup
    procedure.closed_mail = closed_mail&.dup
    procedure.refused_mail = refused_mail&.dup
    procedure.without_continuation_mail = without_continuation_mail&.dup
    procedure.ask_birthday = false # see issue #4242

    procedure.cloned_from_library = from_library
    procedure.parent_procedure = self
    procedure.canonical_procedure = nil

    if from_library
      procedure.service = nil
    elsif self.service.present? && is_different_admin
      procedure.service = self.service.clone_and_assign_to_administrateur(admin)
    end

    procedure.save
    procedure.draft_types_de_champ.update_all(revision_id: procedure.draft_revision.id)
    procedure.draft_types_de_champ_private.update_all(revision_id: procedure.draft_revision.id)
    types_de_champ_in_repetition = TypeDeChamp.where(parent: procedure.draft_types_de_champ.repetition + procedure.draft_types_de_champ_private.repetition)
    types_de_champ_in_repetition.update_all(revision_id: procedure.draft_revision.id)
    types_de_champ_in_repetition.each(&:migrate_parent!)

    if is_different_admin || from_library
      procedure.draft_types_de_champ.each { |tdc| tdc.options&.delete(:old_pj) }
    end

    procedure
  end

  def whitelisted?
    whitelisted_at.present?
  end

  def total_dossier
    self.dossiers.state_not_brouillon.size
  end

  def export_filename(format)
    procedure_identifier = path || "procedure-#{id}"
    "dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
  end

  def export(dossiers)
    ProcedureExportService.new(self, dossiers)
  end

  def to_csv(dossiers)
    export(dossiers).to_csv
  end

  def to_xlsx(dossiers)
    export(dossiers).to_xlsx
  end

  def to_ods(dossiers)
    export(dossiers).to_ods
  end

  def procedure_overview(start_date, groups)
    ProcedureOverview.new(self, start_date, groups)
  end

  def initiated_mail_template
    initiated_mail || Mails::InitiatedMail.default_for_procedure(self)
  end

  def received_mail_template
    received_mail || Mails::ReceivedMail.default_for_procedure(self)
  end

  def closed_mail_template
    closed_mail || Mails::ClosedMail.default_for_procedure(self)
  end

  def refused_mail_template
    refused_mail || Mails::RefusedMail.default_for_procedure(self)
  end

  def without_continuation_mail_template
    without_continuation_mail || Mails::WithoutContinuationMail.default_for_procedure(self)
  end

  def mail_template_for(state)
    case state
    when Dossier.states.fetch(:en_construction)
      initiated_mail_template
    when Dossier.states.fetch(:en_instruction)
      received_mail_template
    when Dossier.states.fetch(:accepte)
      closed_mail_template
    when Dossier.states.fetch(:refuse)
      refused_mail_template
    when Dossier.states.fetch(:sans_suite)
      without_continuation_mail_template
    else
      raise "Unknown dossier state: #{state}"
    end
  end

  def self.default_sort
    {
      'table' => 'self',
      'column' => 'id',
      'order' => 'desc'
    }
  end

  def whitelist!
    touch(:whitelisted_at)
  end

  def closed_mail_template_attestation_inconsistency_state
    # As an optimization, don’t check the predefined templates (they are presumed correct)
    if closed_mail.present?
      tag_present = closed_mail.body.to_s.include?("--lien attestation--")
      if attestation_template&.activated? && !tag_present
        :missing_tag
      elsif !attestation_template&.activated? && tag_present
        :extraneous_tag
      end
    end
  end

  def populate_champ_stable_ids
    TypeDeChamp
      .joins(:revisions)
      .where(procedure_revisions: { procedure_id: id }, stable_id: nil)
      .find_each do |type_de_champ|
        type_de_champ.update_column(:stable_id, type_de_champ.id)
      end
  end

  def missing_steps
    result = []

    if service.nil?
      result << :service
    end

    if missing_instructeurs?
      result << :instructeurs
    end

    result
  end

  def process_dossiers!
    case declarative_with_state
    when Procedure.declarative_with_states.fetch(:en_instruction)
      dossiers
        .state_en_construction
        .where(declarative_triggered_at: nil)
        .find_each(&:passer_automatiquement_en_instruction!)
    when Procedure.declarative_with_states.fetch(:accepte)
      dossiers
        .state_en_construction
        .where(declarative_triggered_at: nil)
        .find_each(&:accepter_automatiquement!)
    end
  end

  def logo_url
    if logo.attached?
      Rails.application.routes.url_helpers.url_for(logo)
    else
      ActionController::Base.helpers.image_url(PROCEDURE_DEFAULT_LOGO_SRC)
    end
  end

  def missing_instructeurs?
    !AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
  end

  def routee?
    routing_enabled? || groupe_instructeurs.size > 1
  end

  def instructeurs_self_management?
    routee? || instructeurs_self_management_enabled?
  end

  def defaut_groupe_instructeur_for_new_dossier
    if !routee? || feature_enabled?(:procedure_routage_api)
      defaut_groupe_instructeur
    end
  end

  def can_be_deleted_by_administrateur?
    brouillon? || dossiers.state_instruction_commencee.empty?
  end

  def can_be_deleted_by_manager?
    kept? && can_be_deleted_by_administrateur?
  end

  def discard_and_keep_track!(author)
    if brouillon?
      reset!
    elsif publiee?
      close!
    end

    dossiers.each do |dossier|
      dossier.discard_and_keep_track!(author, :procedure_removed)
    end

    discard!
  end

  def restore(author)
    if discarded? && undiscard
      dossiers.with_discarded.discarded.find_each do |dossier|
        dossier.restore_if_discarded_with_procedure(author)
      end
    end
  end

  def flipper_id
    "Procedure;#{id}"
  end

  def api_entreprise_role?(role)
    APIEntrepriseToken.new(api_entreprise_token).role?(role)
  end

  def api_entreprise_token
    self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key]
  end

  def api_entreprise_token_expired?
    APIEntrepriseToken.new(api_entreprise_token).expired?
  end

  def create_new_revision
    draft_revision
      .deep_clone(include: [:revision_types_de_champ, :revision_types_de_champ_private])
      .tap(&:save!)
  end

  def average_dossier_weight
    if dossiers.termine.any?
      dossiers_sample = dossiers.termine.limit(100)
      total_size = Champ
        .includes(piece_justificative_file_attachment: :blob)
        .where(type: Champs::PieceJustificativeChamp.to_s, dossier: dossiers_sample)
        .sum('active_storage_blobs.byte_size')

      MIN_WEIGHT + total_size / dossiers_sample.length
    else
      nil
    end
  end

  def publish_revision!
    update!(draft_revision: create_new_revision, published_revision: draft_revision)
    published_revision.touch(:published_at)
    dossiers.state_brouillon.find_each do |dossier|
      DossierRebaseJob.perform_later(dossier)
    end
  end

  def cnaf_enabled?
    api_particulier_sources['cnaf'].present?
  end

  def dgfip_enabled?
    api_particulier_sources['dgfip'].present?
  end

  def pole_emploi_enabled?
    api_particulier_sources['pole_emploi'].present?
  end

  private

  def validate_for_publication?
    validation_context == :publication || publiee?
  end

  def before_publish
    assign_attributes(closed_at: nil, unpublished_at: nil)
  end

  def after_publish(canonical_procedure = nil)
    update!(canonical_procedure: canonical_procedure, draft_revision: create_new_revision, published_revision: draft_revision)
    touch(:published_at)
    published_revision.touch(:published_at)
  end

  def after_republish(canonical_procedure = nil)
    touch(:published_at)
  end

  def after_close
    touch(:closed_at)
  end

  def after_unpublish
    touch(:unpublished_at)
  end

  def update_juridique_required
    self.juridique_required ||= (cadre_juridique.present? || deliberation.attached?)
    true
  end

  def check_juridique
    if juridique_required? && (cadre_juridique.blank? && !deliberation.attached?)
      errors.add(:cadre_juridique, " : veuillez remplir le texte de loi ou la délibération")
    end
  end

  def ensure_path_exists
    if self.path.blank?
      self.path = SecureRandom.uuid
    end
  end

  def ensure_defaut_groupe_instructeur
    if self.groupe_instructeurs.empty?
      groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
    end
  end
end