class Admission::Applicant < ApplicationRecord
  include Admissions::ApplicantApplicationBroadcastable
  include Personable
  include Validatable
  include SycamoreStorage

  audited

  attr_accessor :race

  enum state: { unsubmitted: 0, submitted: 1, accepted: 2, enrolled: 3, closed: 4 }
  enum ethnicity: [:unspecified, :non_hispanic, :hispanic]
  enum gender: { male: 'M', female: 'F', non_binary: 'N' }
  enum payment_status: { in_person: 0, online: 1, exempt: 2 }

  belongs_to :application
  belongs_to :family, class_name: '::Family'
  belongs_to :status, optional: true
  belongs_to :student, optional: true, autosave: true, inverse_of: :admission_applicants

  has_many :applicant_agreements, dependent: :destroy
  has_many :applicant_checkboxes, dependent: :destroy
  has_many :applicant_documents, dependent: :destroy
  has_many :applicant_essays, dependent: :destroy
  has_many :applicant_races, dependent: :destroy
  has_many :additional_values, dependent: :destroy

  has_many :agreements, through: :applicant_agreements
  has_many :checkboxes, through: :applicant_checkboxes
  has_many :documents, through: :applicant_documents
  has_many :essays, through: :applicant_essays
  has_many :races, through: :applicant_races
  has_many :application_agreements, through: :application
  has_many :application_essays, through: :application
  has_many :application_checkboxes, through: :application
  has_many :attached_tags, class_name: '::AttachedTag', as: :associated, inverse_of: :associated
  has_many :tags, through: :attached_tags, dependent: :destroy
  has_many :connection_logs, class_name: '::ConnectionLog', as: :associated, dependent: :destroy

  has_one :applicant_medical, dependent: :destroy

  delegate :school, to: :family
  delegate :school_year, to: :application
  delegate :name, to: :family, prefix: true
  delegate :name, :phone, to: :school, prefix: true

  has_many_attached :documents

  has_one_attachment :admission_application
  has_one_attached :submitted_application

  accepts_nested_attributes_for :student, update_only: true

  after_initialize :set_application, if: :new_record?

  before_validation :sync_changes, if: -> { new_record? && student_id? }
  before_validation :set_ethnicity

  before_create :set_needs_review, if: :unsubmitted?
  before_create :sync_changes, if: :student_id

  before_save :set_submitted_at, if: :state_changed?
  before_save :set_status_default, if: :state_changed?
  before_save :unenroll, if: -> { state_changed? && state_was.to_sym == :enrolled }
  before_save :update_races
  before_save :enroll_student, if: -> { state_changed? && enrolled? }
  before_save :mark_payment_options, if: :payment_status_changed?
  before_save :clear_classes_enrolled, if: :closed?

  after_create :create_student_in_tep, unless: -> { review? || application.enrollment? }

  after_update :create_student_in_tep, if: :saved_change_to_review?,
    unless: -> { review? || application.enrollment? }

  after_commit :enroll_family, if: -> { saved_change_to_state? && enrolled? }

  before_destroy :destroy_family_admission_data

  validates :first_name, :last_name, :grade, presence: true
  validates :middle_name, length: { maximum: 24 }
  validates :first_name, :last_name, :nickname, length: { maximum: 36 }
  validates :gender,
    inclusion: { in: ['male', 'female', 'non_binary'], message: '%{value} is not a valid gender' },
    allow_blank: true

  validate :state_change_allowed, if: :state_changed?
  validate :state_and_status_match, if: :status_id_changed?
  validate :valid_grade?

  scope :ordered, -> { order(:last_name, :first_name) }
  scope :with_grade, ->(grade) { where(grade: grade) if grade.present? }
  scope :with_state, ->(state) { where(state: state) if state.present? }
  scope :with_application, ->(id) { where(application_id: id) if id.present? }
  scope :with_status, ->(status) { where(status_id: status) if status.present? }
  scope :with_returning, ->(value) { where(returning: value) if value.present? }
  scope :by_review, ->(flag) { where(review: flag) unless flag.nil? }
  scope :with_tags, ->(ids) { left_joins(:tags).where(tags: { id: ids }) if ids.present? }
  scope :by_payment_status, ->(status) { where(payment_status: status) }

  scope :ordered_by_system_status, -> do
    includes(:status).order(:state).order('admission_statuses.name')
  end

  scope :with_system_status, ->(system_status) do
    return if system_status.blank?

    if system_status.include?('_none')
      state = system_status.sub('_none', '')
      where(state: state, status_id: nil)
    else
      is_state = Admission::Applicant.states.key?(system_status)
      is_state ? with_state(system_status) : with_status(system_status)
    end
  end

  scope :by_school_year, ->(id) do
    return if id.nil?

    joins(:application).where(admission_applications: { school_year_id: id })
  end

  scope :by_enrollment, ->(value) do
    return if value.nil?

    joins(:application).where(admission_applications: { enrollment: value })
  end

  scope :by_application_payment, ->(flag=true) do
    return if flag

    joins(:application).where(admission_applications: { payment: true })
  end

  scope :by_payment_paid, ->(flag) do
    return if flag

    where(payment_status: [:in_person, :online, :exempt])
  end

  def self.ethnicity_labels
    {
      unspecified: '',
      non_hispanic: 'Non Hispanic/Latino',
      hispanic: 'Hispanic/Latino'
    }
  end

  def migrate_pdf
    return if submitted_application.attached? || admission_application.nil?

    submitted_application.attach(
      io: File.open(admission_application.file_path),
      filename: 'Admission Application'
    )
  end

  def profile_fields_complete?
    profile_fields = application.profile_fields.where(required: true).pluck(:field)
    profile_fields.each do |field|
      return false if field == 'race' && races.any? || public_send(field) == ''
    end
    true
  end

  def additional_fields_complete?
    additional_fields = application.additional_fields
      .where(required: true)
      .pluck(:student_additional_field_id)

    values_by_field = additional_values.index_by(&:student_additional_field_id)

    additional_fields.each do |field_id|
      value = values_by_field[field_id]
      return false if value.nil? || value.value.blank?
    end

    true
  end

  def agreements_complete?
    incomplete_agreements.empty?
  end

  def essays_complete?
    incomplete_essays.empty?
  end

  def documents_complete?
    incomplete_documents.empty?
  end

  def medical_complete?
    return true unless application.require_medical
    return false if applicant_medical.nil?

    applicant_medical.health_issues.each_value do |value|
      return true if value.present?
    end

    false
  end

  def next_agreement(agreement=nil)
    completed_ids = applicant_agreements.pluck(:agreement_id)
    position =
      agreement.nil? ? 0 : application.application_agreements.find_by(agreement: agreement).position
    application.agreements.where('admission_application_agreements.position > ?', position)
      .where.not(id: completed_ids).order('admission_application_agreements.position').first
  end

  def next_essay(essay=nil)
    completed_ids = applicant_essays.pluck(:essay_id)
    position = essay.nil? ? 0 : application.application_essays.find_by(essay: essay).position
    application.essays.where('admission_application_essays.position > ?', position)
      .where.not(id: completed_ids).order('admission_application_essays.position').first
  end

  def next_document(document=nil)
    completed_ids = applicant_documents.pluck(:document_id)
    position =
      document.nil? ? 0 : application.application_documents.find_by(document: document).position
    application.documents.where('admission_application_documents.position > ?', position)
      .where.not(id: completed_ids).order('admission_application_documents.position').first
  end

  def ethnicity_to_i
    self.class.ethnicities[ethnicity]
  end

  def sync_changes(is_applicant=true)
    fields = Admission::ProfileField.fields.keys - ['application_grade', 'race']
    fields.each do |field|
      value = (is_applicant ? student : self).public_send(field)
      next if value.nil?

      if field.to_sym == :nickname
        if is_applicant
          self.nickname = value&.first_name
        else
          student_nickname = student.find_or_build_nickname
          nickname.blank? ? student_nickname.destroy : student_nickname.first_name = nickname
        end
      else
        value = ethnicity_to_i if field == 'ethnicity' && !is_applicant
        is_applicant ? self[field] = value : student[field] = value
      end
    end

    if is_applicant
      self.races = student.races
    else
      student.races = races
    end
  end

  def displayable_components
    @displayable_components ||= {
      welcome: true,
      general: true,
      medical: application.require_medical,
      downloads: application.application_attachments.any?,
      agreements: application.application_agreements.any?,
      essays: application.application_essays.any?,
      documents: application.application_documents.any?,
      additional_fields: application.additional_fields.any?,
      family_revisions: school.admission_family_revision_fields.by_display_fields.present?,
      family_additional_fields: school.admission_family_additional_fields.with_display.present?,
      medical_revisions: school.admission_medical_revision_fields.with_display.present?,
      contact_revisions: true,
      review_application: true
    }
  end

  def incomplete_contacts?
    require_emergency_contact = application.school.admission_contact_revision_fields
      .emergency_contact
      .where(required: true)

    return true if require_emergency_contact.empty?

    admissions_contact = family.admission_contact_revisions
      .with_non_primary_emergency_contact(school_year)
    back_office_contact = family.contacts.with_non_primary_emergency

    if family.admission_contact_revisions.present?
      !!admissions_contact
    else
      !!back_office_contact
    end
  end

  def submit
    update(state: :submitted)
    emails = family.primary_contact_emails
    emails += family.admission_contact_revisions.where(primary: true).pluck(:email)
    emails.uniq.each { |e| Admissions::SubmittedEmailJob.perform_async(id, e) }
    Reporting::Admissions::SubmittedApplicationJob.perform_async(school.id, nil, id: id)

    return true unless application.application_email&.email?

    email = application.application_email.email
    Mailgun::TemplateJob.perform_async(
      'Sycamore School <noreply@sycamoreschool.com>',
      email,
      'Pre-Applicant Ready for Review',
      'admissions-application-submitted-internal',
      variables: {
        school_name: school.name,
        family_name: family.name,
        applicant_name: full_name,
        applicant_grade: decorate.grade_level,
        application_name: application.name
      },
      tags: [school.id.to_s, 'admissions', 'application-submitted']
    )
  end

  def reviewed_prop(prop, arg=nil)
    object = review || student.nil? ? decorate : student.decorate
    return object.send(prop) if arg.nil?

    object.send(prop, arg)
  end

  private
    def set_needs_review
      self.review = true
    end

    def set_submitted_at
      if submitted?
        self.submitted_at = Time.current
      elsif unsubmitted?
        self.submitted_at = nil
      end
    end

    def set_status_default
      return if status&.state == state

      self.status_id = nil
    end

    def state_change_allowed
      case state_was
      when 'unsubmitted', 'closed'
        errors.add(:base, 'Applicant must be opened') unless submitted? || closed?
      end
    end

    def state_and_status_match
      return if status_id.nil? || status.state == state

      errors.add(:base, 'Status must reflect system state')
    end

    def incomplete_agreements
      application.agreements.joins(<<~SQL)
        LEFT OUTER JOIN admission_applicant_agreements
        ON admission_application_agreements.agreement_id =
          admission_applicant_agreements.agreement_id
        AND admission_applicant_agreements.applicant_id = #{id}
      SQL
        .where(admission_applicant_agreements: { id: nil })
        .order('admission_application_agreements.position')
    end

    def incomplete_essays
      application.essays.joins(<<~SQL)
        LEFT OUTER JOIN admission_applicant_essays
        ON admission_application_essays.essay_id =
          admission_applicant_essays.essay_id
        AND admission_applicant_essays.applicant_id = #{id}
      SQL
        .where(admission_applicant_essays: { id: nil })
        .where(admission_application_essays: { required: true })
        .order('admission_application_essays.position')
    end

    def incomplete_documents
      application.documents.joins(<<~SQL)
        LEFT OUTER JOIN admission_applicant_documents
        ON admission_application_documents.document_id =
          admission_applicant_documents.document_id
        AND admission_applicant_documents.applicant_id = #{id}
      SQL
        .where(admission_applicant_documents: { id: nil })
        .where(admission_application_documents: { required: true })
        .order('admission_application_documents.position')
    end

    def destroy_family_admission_data
      family.destroy_admission_data if family.admission_applicants.count == 1
    end

    def update_races
      self.races = Race.where(id: race) if race
    end

    def enroll_student
      student.school_year_students
        .find_or_initialize_by(current: true, school_year: school_year)
        .update(grade_id: grade)
    end

    def enroll_family
      return unless school_year.current?

      family.school_year_families
        .find_or_create_by(school_year: school_year)
    end

    def set_application
      find_params = { open: true, enrollment: student_id.present? }
      key = returning ? :returning_students : :new_students
      find_params[key] = true
      self.application = school.admission_school_year
        .admission_applications
        .for_grade(grade)
        .find_by(find_params)
    end

    def set_ethnicity
      self.ethnicity = 'unspecified' unless ethnicity
    end

    def unenroll
      year = student.school_year_students.find_by(current: true, school_year: school_year)
      return if year.nil?
      return year.destroy if school.school_config.sessions? || year.entry_date > Time.zone.now

      year.current = false
      year.set_exit_date
      year.save!
    end

    def valid_grade?
      range = (school.first_grade_offered..school.last_grade_offered).to_a
      if !range.include?(grade)
        errors.add(:grade, 'is not valid for this school')
      end
    end

    def mark_payment_options
      return if exempt?

      if payment_status.nil?
        self.paid_at = nil
        self.payment_amount = nil
        self.payment_description = nil
      else
        self.paid_at = Date.current
        self.payment_amount = application.payment_amount
        self.payment_description = application.payment_description
      end
    end

    def clear_classes_enrolled
      return unless student

      student.class_student_enrollments.where(school_year: school_year).destroy_all
    end

    def create_student_in_tep
      return if student_id.nil? || !school.school_config.tep_integration?

      # Send student as inactive when an entrance applicant is approved
      Tep::StudentService.call(school, student_id, status: :inactive)
    end
end
