# frozen_string_literal: true

#  Copyright (c) 2012-2024, Jungwacht Blauring Schweiz. This file is part of
#  hitobito and licensed under the Affero General Public License version 3
#  or later. See the COPYING file at the top-level directory or at
#  https://github.com/hitobito/hitobito.

class PeopleController < CrudController
  include RenderPeopleExports
  include AsyncDownload
  include Tags
  prepend RenderTableDisplays

  self.nesting = Group

  self.remember_params += [:name, :range, :filters, :filter_id]

  self.permitted_attrs = [:first_name, :last_name, :company_name, :nickname, :company,
    :gender, :birthday, :additional_information, :picture, :remove_picture] +
    Contactable::ACCESSIBLE_ATTRS +
    [family_members_attributes: [:id, :kind, :other_id, :_destroy]]
  FeatureGate.if(:person_language) do
    permitted_attrs << :language
  end

  # required to allow api calls
  protect_from_forgery with: :null_session, only: [:index, :show]

  decorates :group, :person, :add_requests

  helper_method :index_full_ability?

  # load group before authorization
  prepend_before_action :parent

  prepend_before_action :entry, only: [:show, :edit, :update, :destroy,
    :send_password_instructions, :primary_group]

  before_action :index_archived, only: :index, if: :group_archived_and_no_filter

  after_save :show_email_change_info

  before_render_show :load_person_add_requests, if: -> { html_request? }
  before_render_index :load_people_add_requests, if: -> { html_request? }

  helper_method :list_filter_args

  def index # rubocop:disable Metrics/AbcSize we support a lot of formats, hence many code-branches
    respond_to do |format|
      format.html { @people = decorated_entries }
      format.pdf { render_pdf_in_background(filter_entries, group, "people_#{group.id}") }
      format.csv { render_tabular_entries_in_background(:csv) }
      format.xlsx { render_tabular_entries_in_background(:xlsx) }
      format.vcf { render_vcf(filter_entries.includes(:phone_numbers)) }
      format.email { render_emails(filter_entries, ",") }
      format.email_outlook { render_emails(filter_entries, ";") }
      format.json { render_entries_json(filter_entries) }
    end
  end

  def show
    respond_to do |format|
      format.html
      format.pdf { render_pdf([entry], group) }
      format.csv { render_tabular_entry(:csv) }
      format.xlsx { render_tabular_entry(:xlsx) }
      format.vcf { render_vcf([entry]) }
      format.json { render_entry_json }
    end
  end

  # POST button, send password instructions
  def send_password_instructions
    msg = send_login_job(entry, current_user)

    respond_to do |format|
      format.html { redirect_to group_person_path(group, entry), *msg }
      format.js do
        flash.now[msg.keys.first] = msg.values.first
        render "shared/update_flash"
      end
    end
  end

  # PUT button, ajax
  def primary_group # rubocop:disable Metrics/AbcSize
    success = entry.update(primary_group_id: params[:primary_group_id])
    respond_to do |format|
      format.html { redirect_to group_person_path(group, entry) }
      format.js do
        return render :primary_group if success

        flash.now.alert = I18n.t("global.errors.header", count: entry.errors.size)
        render "shared/update_flash"
      end
    end
  end

  # public for serializer
  def index_full_ability?
    if params[:range].blank? || params[:range] == "group"
      can?(:index_full_people, @group)
    else
      can?(:index_deep_full_people, @group)
    end
  end

  # dont use class level accessor as expression is evaluated whenever constant is
  # loaded which might be before wagon that defines groups / roles has been loaded
  def self.sort_mappings_with_indifferent_access
    {roles: {
      joins: [:roles, "INNER JOIN role_type_orders ON roles.type = role_type_orders.name"],
      order: ["role_type_orders.order_weight", "company_name", "last_name", "first_name", "nickname"]
    }}.with_indifferent_access
  end

  private

  alias_method :group, :parent
  # every person may be displayed underneath the root group,
  # even if it does not directly belong to it.
  def find_entry
    return Person.find(params[:id]) if params[:action] == "destroy" || group&.root?

    super
  end

  def decorated_entries
    list = prepare_entries(filter_entries).page(params[:page])
    PaginatingDecorator.new(
      list,
      with: PersonDecorator,
      context: {blocked_emails: load_blocked_emails(list)}
    )
  end

  def assign_attributes
    if model_params.present? && !can?(:update_email, entry)
      model_params.delete(:email)
    end
    super
  end

  def load_people_add_requests
    if params[:range].blank? && can?(:create, @group.roles.new)
      @person_add_requests = @group.person_add_requests.list
        .includes(person: :primary_group)
        .select("person_add_requests.*")
    end
  end

  def load_person_add_requests
    if can?(:update, entry)
      @add_requests = entry.add_requests.includes(:body, requester: {roles: :group})
      set_add_request_status_notification if show_add_request_status?
    end
  end

  def load_blocked_emails(people)
    emails = people.flat_map do |person|
      [person.email, *person.additional_emails.map(&:value)]
    end.uniq.compact
    Bounce.blocked_set(emails)
  end

  def show_add_request_status?
    flash[:notice].blank? && flash[:alert].blank? &&
      params[:body_type].present? && params[:body_id].present?
  end

  def set_add_request_status_notification
    status = Person::AddRequest::Status.for(entry.id, params[:body_type], params[:body_id])
    return if status.pending?

    if status.created?
      flash.now[:notice] = status.approved_message
    else
      flash.now[:alert] = status.rejected_message
    end
  end

  def filter_entries
    entries = add_table_display_to_query(person_filter.entries, current_person, group)
    sort_by_sort_expression(entries)
  end

  def list_filter_args
    if params[:filter_id]
      PeopleFilter.for_group(group).find(params[:filter_id]).to_params
    else
      params
    end
  end

  def prepare_entries(entries)
    entries = if index_full_ability?
      entries.includes(:additional_emails, :phone_numbers)
    else
      entries.preload_public_accounts
    end

    entries.preload_picture
  end

  def render_tabular_entries_in_background(format)
    full = params[:details].present? && index_full_ability?
    with_async_download_cookie(format, :people_export) do |filename|
      render_tabular_in_background(format, full, filename)
    end
  end

  def render_tabular_entry(format)
    render_tabular(format, [entry], params[:details].present? && can?(:show_full, entry))
  end

  def render_tabular_in_background(format, full, filename)
    Export::PeopleExportJob.new(
      format, current_person.id, @group.id, list_filter_args,
      tabular_params(full: full, filename: filename)
    ).enqueue!
  end

  def tabular_params(**opts)
    params.slice(:household, :selection).permit!.merge(opts)
  end

  def render_tabular(format, entries, full)
    exporter = Export::Tabular::People::Households if params[:household]
    exporter ||= full ? Export::Tabular::People::PeopleFull : Export::Tabular::People::PeopleAddress
    send_data exporter.export(format, entries), type: format
  end

  def render_entries_json(entries)
    render json: ListSerializer.new(prepare_entries(entries).includes(:social_accounts).decorate,
      group: @group,
      multiple_groups: @person_filter.multiple_groups,
      serializer: PeopleSerializer,
      controller: self)
  end

  def render_entry_json
    render json: PersonSerializer.new(entry.decorate, group: @group, controller: self)
  end

  def authorize_class
    authorize!(:index_people, group)
  end

  def person_filter
    @person_filter ||= Person::Filter::List.new(@group, current_user, list_filter_args)
  end

  def send_login_job(entry, current_user)
    if Truemail.valid?(entry.email)
      Person::SendLoginJob.new(entry, current_user).enqueue!
      {notice: I18n.t("#{controller_name}.#{action_name}")}
    else
      {alert: I18n.t("#{controller_name}.#{action_name}_invalid_email")}
    end
  end

  def show_email_change_info
    return unless entry.show_email_change_info?

    flash[:notice] = I18n.t("#{controller_name}.#{action_name}_email_must_be_confirmed",
      new_mail: entry.unconfirmed_email)
  end

  def index_archived
    redirect_to(filters: {role: {include_archived: true}})
  end

  def group_archived_and_no_filter
    group.archived? && list_filter_args[:filters].nil?
  end

  def model_scope
    super.then do |scope|
      (action_name == "show") ? scope.includes(roles: :group) : scope
    end
  end
end
