# frozen_string_literal: true
class CustomWizard::Action
  attr_accessor :submission, :action, :user, :guardian, :result

  REQUIRES_USER = %w[update_profile open_composer watch_categories add_to_group]

  WIZARD_USER = "wizard-user"

  def initialize(opts)
    @wizard = opts[:wizard]
    @action = opts[:action]
    @user = @wizard.user
    @guardian = Guardian.new(@user)
    @submission = opts[:submission]
    @log = []
    @result = CustomWizard::ActionResult.new
  end

  def perform
    if REQUIRES_USER.include?(action["id"]) && !@user
      log_error("action requires user", "id: #{action["id"]};")
      @result.success = false
      return @result
    end

    ActiveRecord::Base.transaction { self.send(action["type"].to_sym) }

    @result.handler.enqueue_jobs if creates_post? && @result.success?

    @submission.fields[action["id"]] = @result.output if @result.success? && @result.output.present?

    save_log

    @result.submission = @submission
    @result
  end

  def mapper_data
    @mapper_data ||= @submission&.fields_and_meta || {}
  end

  def mapper
    @mapper ||= CustomWizard::Mapper.new(user: user, data: mapper_data)
  end

  def callbacks_for(action)
    self.class.callbacks[action] || []
  end

  def create_topic
    params = basic_topic_params.merge(public_topic_params)

    callbacks_for(:before_create_topic).each do |acb|
      params = acb.call(params, @wizard, @action, @submission)
    end

    if params[:title].present? && params[:raw].present?
      creator = PostCreator.new(topic_poster, params)
      post = creator.create

      if creator.errors.present?
        messages = creator.errors.full_messages.join(" ")
        log_error("failed to create", messages)
      elsif action["skip_redirect"].blank?
        @submission.redirect_on_complete = post.topic.url
      end

      if creator.errors.blank?
        log_success("created topic", "id: #{post.topic.id}")
        result.handler = creator
        result.output = post.topic.id
      end
    else
      log_error("invalid topic params", "title: #{params[:title]}; post: #{params[:raw]}")
    end
  end

  def send_message
    if action["required"].present?
      required =
        CustomWizard::Mapper.new(inputs: action["required"], data: mapper_data, user: user).perform

      if required.blank?
        log_error("required input not present")
        return
      end
    end

    params = basic_topic_params

    targets =
      CustomWizard::Mapper.new(
        inputs: action["recipient"],
        data: mapper_data,
        user: user,
        multiple: true,
      ).perform

    if targets.blank?
      log_error("no recipients", "send_message has no recipients")
      return
    end

    params[:target_group_names] = []
    params[:target_usernames] = []
    params[:target_emails] = []
    [*targets].each do |target|
      if Group.find_by(name: target)
        params[:target_group_names] << target
      elsif User.find_by_username(target)
        params[:target_usernames] << target
      elsif target.match(/@/) # Compare discourse/discourse/app/controllers/posts_controller.rb#L922-L923
        params[:target_emails] << target
      end
    end

    if params[:title].present? && params[:raw].present? &&
         (
           params[:target_usernames].present? || params[:target_group_names].present? ||
             params[:target_emails].present?
         )
      params[:archetype] = Archetype.private_message

      creator = PostCreator.new(topic_poster, params)
      post = creator.create

      if creator.errors.present?
        messages = creator.errors.full_messages.join(" ")
        log_error("failed to create message", messages)
      elsif user && action["skip_redirect"].blank?
        @submission.redirect_on_complete = post.topic.url
      end

      if creator.errors.blank?
        log_success("created message", "id: #{post.topic.id}")
        result.handler = creator
        result.output = post.topic.id
      end
    else
      log_error(
        "invalid message params",
        "title: #{params[:title]}; post: #{params[:raw]}; recipients: #{params[:target_usernames]}",
      )
    end
  end

  def update_profile
    params = {}

    if (profile_updates = action["profile_updates"])
      profile_updates.first[:pairs].each do |pair|
        if allowed_profile_field?(pair["key"])
          key = cast_profile_key(pair["key"])
          value =
            cast_profile_value(mapper.map_field(pair["value"], pair["value_type"]), pair["key"])

          if user_field?(pair["key"])
            params[:custom_fields] ||= {}
            params[:custom_fields][key] = value
          else
            params[key.to_sym] = value
          end
        end
      end
    end

    params = add_custom_fields(params)

    if params.present?
      result = UserUpdater.new(Discourse.system_user, user).update(params)

      result = update_avatar(params[:avatar]) if params[:avatar].present?

      if result
        log_success("updated profile fields", "fields: #{params.keys.map(&:to_s).join(",")}")
      else
        log_error("failed to update profile fields", "result: #{result.inspect}")
      end
    else
      log_error("invalid profile fields params", "params: #{params.inspect}")
    end
  end

  def watch_tags
    tags = CustomWizard::Mapper.new(inputs: action["tags"], data: mapper_data, user: user).perform

    tags = [*tags]
    level = action["notification_level"].to_sym

    if level.blank?
      log_error("Notifcation Level was not set. Exiting watch tags action")
      return
    end

    users = []

    if action["usernames"]
      mapped_users =
        CustomWizard::Mapper.new(inputs: action["usernames"], data: mapper_data, user: user).perform

      if mapped_users.present?
        mapped_users = mapped_users.split(",").map { |username| User.find_by(username: username) }
        users.push(*mapped_users)
      end
    end

    users.push(user) if ActiveRecord::Type::Boolean.new.cast(action["wizard_user"])

    users.each do |user|
      result = TagUser.batch_set(user, level, tags)

      if result
        log_success("#{user.username} notifications for #{tags} set to #{level}")
      else
        log_error("failed to set #{user.username} notifications for #{tags} to #{level}")
      end
    end
  end

  def watch_categories
    watched_categories =
      CustomWizard::Mapper.new(inputs: action["categories"], data: mapper_data, user: user).perform

    watched_categories = [*watched_categories].map(&:to_i)

    notification_level = action["notification_level"]

    if notification_level.blank?
      log_error("Notifcation Level was not set. Exiting wizard action")
      return
    end

    mute_remainder =
      CustomWizard::Mapper.new(
        inputs: action["mute_remainder"],
        data: mapper_data,
        user: user,
      ).perform

    users = []

    if action["usernames"]
      mapped_users =
        CustomWizard::Mapper.new(inputs: action["usernames"], data: mapper_data, user: user).perform

      if mapped_users.present?
        mapped_users = mapped_users.split(",").map { |username| User.find_by(username: username) }
        users.push(*mapped_users)
      end
    end

    users.push(user) if ActiveRecord::Type::Boolean.new.cast(action["wizard_user"])

    category_ids = Category.all.pluck(:id)
    set_level = CategoryUser.notification_levels[notification_level.to_sym]
    mute_level = CategoryUser.notification_levels[:muted]

    users.each do |user|
      category_ids.each do |category_id|
        new_level = nil

        if watched_categories.include?(category_id) && set_level != nil
          new_level = set_level
        elsif mute_remainder
          new_level = mute_level
        end

        CategoryUser.set_notification_level_for_category(user, new_level, category_id) if new_level
      end

      if watched_categories.any?
        log_success("#{user.username} notifications for #{watched_categories} set to #{set_level}")
      end

      log_success("#{user.username} notifications for all other categories muted") if mute_remainder
    end
  end

  def send_to_api
    api_body = nil

    if action["api_body"] != ""
      begin
        api_body_parsed = JSON.parse(action["api_body"])
      rescue JSON::ParserError
        raise Discourse::InvalidParameters,
              "Invalid API body definition: #{action["api_body"]} for #{action["title"]}"
      end
      api_body = JSON.parse(mapper.interpolate(JSON.generate(api_body_parsed)))
    end

    result =
      CustomWizard::Api::Endpoint.request(user, action["api"], action["api_endpoint"], api_body)

    if error = result["error"] || (result[0] && result[0]["error"])
      error = error["message"] || error
      log_error("api request failed", "message: #{error}")
    else
      log_success("api request succeeded", "result: #{result}")
    end
  end

  def open_composer
    params = basic_topic_params

    if params[:title].present? && params[:raw].present?
      url = "/new-topic?title=#{encode_query_param(params[:title])}"
      url += "&body=#{encode_query_param(params[:raw])}"

      if category_id = action_category
        url += "&category_id=#{category_id}"
      end

      if tags = action_tags
        url += "&tags=#{tags.join(",")}"
      end

      route_to = Discourse.base_uri + url
      @result.output = @submission.route_to = route_to

      log_success("route: #{route_to}")
    else
      log_error("invalid composer params", "title: #{params[:title]}; post: #{params[:raw]}")
    end
  end

  def add_to_group
    group_map =
      CustomWizard::Mapper.new(
        inputs: action["group"],
        data: mapper_data,
        user: user,
        opts: {
          multiple: true,
        },
      ).perform

    group_map = group_map.flatten.compact

    if group_map.blank?
      log_error("invalid group map")
      return
    end

    groups =
      group_map.reduce([]) do |result, g|
        begin
          result.push(Integer(g))
        rescue ArgumentError
          group = Group.find_by(name: g)
          result.push(group.id) if group
        end

        result
      end

    result = nil

    if groups.present?
      groups.each do |group_id|
        group = Group.find_by(id: group_id) if group_id
        result = group.add(user) if group
      end
    end

    if result
      log_success("added to groups", "groups: #{groups.map(&:to_s).join(",")}")
    else
      detail = groups.present? ? "groups: #{groups.map(&:to_s).join(",")}" : nil
      log_error("failed to add to groups", detail)
    end
  end

  def route_to
    return if (url_input = action["url"]).blank?

    if url_input.is_a?(String)
      url = mapper.interpolate(url_input)
    else
      url = CustomWizard::Mapper.new(inputs: url_input, data: mapper_data, user: user).perform
    end

    if action["code"].present?
      @submission.fields[action["code"]] = SecureRandom.hex(8)
      url += "&#{action["code"]}=#{@submission.fields[action["code"]]}"
    end

    route_to = UrlHelper.encode(url)
    @submission.route_to = route_to

    log_info("route: #{route_to}")
  end

  def create_group
    group =
      begin
        Group.new(new_group_params.except(:usernames, :owner_usernames))
      rescue ArgumentError => e
        raise Discourse::InvalidParameters, "Invalid group params"
      end

    if group.save
      def get_user_ids(username_string)
        User.where(username: username_string.split(",")).pluck(:id)
      end

      if new_group_params[:owner_usernames].present?
        owner_ids = get_user_ids(new_group_params[:owner_usernames])
        owner_ids.each { |user_id| group.group_users.build(user_id: user_id, owner: true) }
      end

      if new_group_params[:usernames].present?
        user_ids = get_user_ids(new_group_params[:usernames])
        if user_ids.count < new_group_params[:usernames].count
          log_error("Warning, group creation: some users were not found!")
        end
        user_ids -= owner_ids if owner_ids
        user_ids.each { |user_id| group.group_users.build(user_id: user_id) }
      end

      log_success("Group created", group.name) if group.save

      result.output = group.name
    else
      log_error("Group creation failed", group.errors.messages)
    end
  end

  def create_category
    category =
      begin
        Category.new(new_category_params.merge(user: user))
      rescue ArgumentError => e
        raise Discourse::InvalidParameters, "Invalid category params"
      end

    if category.save
      StaffActionLogger.new(user).log_category_creation(category)
      log_success("Category created", category.name)
      result.output = category.id
    else
      log_error("Category creation failed", category.errors.messages)
    end
  end

  def self.callbacks
    @callbacks ||= {}
  end

  def self.register_callback(action, &block)
    callbacks[action] ||= []
    callbacks[action] << block
  end

  private

  def action_category
    output =
      CustomWizard::Mapper.new(inputs: action["category"], data: mapper_data, user: user).perform

    return false if output.blank?

    if output.is_a?(Array)
      output.first
    elsif output.is_a?(Integer)
      output
    elsif output.is_a?(String)
      output.to_i
    end
  end

  def action_tags
    output = CustomWizard::Mapper.new(inputs: action["tags"], data: mapper_data, user: user).perform

    return false if output.blank?

    if output.is_a?(Array)
      output.flatten
    else
      output.is_a?(String)
      [*output]
    end
  end

  def add_custom_fields(params = {})
    if (custom_fields = action["custom_fields"]).present?
      field_map =
        CustomWizard::Mapper.new(inputs: custom_fields, data: mapper_data, user: user).perform
      registered_fields = CustomWizard::CustomField.full_list

      field_map.each do |field|
        keyArr = field[:key].split(".")
        value = field[:value]

        if keyArr.length > 1
          klass = keyArr.first.to_sym
          name = keyArr.second

          if keyArr.length === 3 && name.include?("{}")
            name = name.gsub("{}", "")
            json_attr = keyArr.last
            type = :json
          end
        else
          name = keyArr.first
        end

        registered = registered_fields.select { |f| f.name == name }.first
        if registered.present?
          klass = registered.klass.to_sym
          type = registered.type.to_sym
        end

        next if type === :json && json_attr.blank?

        if klass === :topic
          params[:topic_opts] ||= {}
          params[:topic_opts][:custom_fields] ||= {}

          if type === :json
            params[:topic_opts][:custom_fields][name] ||= {}
            params[:topic_opts][:custom_fields][name][json_attr] = value
          else
            params[:topic_opts][:custom_fields][name] = value
          end
        else
          if type === :json
            params[:custom_fields][name] ||= {}
            params[:custom_fields][name][json_attr] = value
          else
            params[:custom_fields] ||= {}
            params[:custom_fields][name] = value
          end
        end
      end
    end

    params
  end

  def basic_topic_params
    params = {
      skip_validations: true,
      topic_opts: {
        custom_fields: {
          wizard_submission_id: @wizard.current_submission.id,
        },
      },
    }

    params[:title] = CustomWizard::Mapper.new(
      inputs: action["title"],
      data: mapper_data,
      user: user,
    ).perform

    params[:raw] = (
      if action["post_builder"]
        mapper.interpolate(
          action["post_template"],
          user: true,
          value: true,
          wizard: true,
          template: true,
        )
      else
        @submission.fields[action["post"]]
      end
    )

    params[:import_mode] = ActiveRecord::Type::Boolean.new.cast(action["suppress_notifications"])

    add_custom_fields(params)
  end

  def public_topic_params
    params = {}

    if category = action_category
      params[:category] = category
    end

    if tags = action_tags
      params[:tags] = tags
    end

    if public_topic_fields.any?
      public_topic_fields.each do |field|
        unless action[field].nil? || action[field] == ""
          params[field.to_sym] = CustomWizard::Mapper.new(
            inputs: action[field],
            data: mapper_data,
            user: user,
          ).perform
        end
      end
    end

    params
  end

  def topic_poster
    @topic_poster ||=
      begin
        poster_id =
          CustomWizard::Mapper.new(inputs: action["poster"], data: mapper_data, user: user).perform
        poster_id = [*poster_id].first if poster_id.present?

        if poster_id.blank? || poster_id === WIZARD_USER
          poster = user || guest_user
        else
          poster = User.find_by_username(poster_id)
        end

        poster || Discourse.system_user
      end
  end

  def guest_user
    @guest_user ||=
      begin
        return nil unless action["guest_email"]

        email = CustomWizard::Mapper.new(inputs: action["guest_email"], data: mapper_data).perform

        if email&.match(/@/)
          if user = User.find_by_email(email)
            user
          else
            User.create!(
              email: email,
              username: UserNameSuggester.suggest(email),
              name: User.suggest_name(email),
              staged: true,
            )
          end
        end
      end
  end

  def new_group_params
    params = {}

    %w[
      name
      full_name
      title
      bio_raw
      owner_usernames
      usernames
      mentionable_level
      messageable_level
      visibility_level
      members_visibility_level
      grant_trust_level
    ].each do |attr|
      input = action[attr]

      raise ArgumentError.new if attr === "name" && input.blank?

      input = action["name"] if attr === "full_name" && input.blank?

      if input.present?
        value = CustomWizard::Mapper.new(inputs: input, data: mapper_data, user: user).perform

        if value
          value = value.parameterize(separator: "_") if attr === "name"
          value = value.to_i if attr.include?("_level")

          params[attr.to_sym] = value
        end
      end
    end

    add_custom_fields(params)
  end

  def new_category_params
    params = {}

    %w[name slug color text_color parent_category_id permissions].each do |attr|
      if action[attr].present?
        value =
          CustomWizard::Mapper.new(inputs: action[attr], data: mapper_data, user: user).perform

        if value
          value = value[0] if attr === "parent_category_id" && value.is_a?(Array)

          if attr === "permissions" && value.is_a?(Array)
            permissions = value
            value = {}

            permissions.each do |p|
              k = p[:key]
              v = p[:value].to_i

              if k.is_a?(Array)
                group = Group.find_by(id: k[0])
                k = group.name
              else
                k = k.parameterize(separator: "_")
              end

              value[k] = v
            end
          end

          value = value.parameterize(separator: "-") if attr === "slug"

          params[attr.to_sym] = value
        end
      end
    end

    add_custom_fields(params)
  end

  def creates_post?
    %i[create_topic send_message].include?(action["type"].to_sym)
  end

  def public_topic_fields
    ["visible"]
  end

  def profile_url_fields
    %w[profile_background card_background]
  end

  def cast_profile_key(key)
    if profile_url_fields.include?(key)
      "#{key}_upload_url"
    else
      key
    end
  end

  def cast_profile_value(value, key)
    return value if value.nil?

    if profile_url_fields.include?(key)
      value["url"]
    elsif key === "avatar"
      value["id"]
    else
      value
    end
  end

  def profile_excluded_fields
    %w[username email trust_level].freeze
  end

  def allowed_profile_field?(field)
    allowed_profile_fields.include?(field) || user_field?(field)
  end

  def user_field?(field)
    field.to_s.include?(::User::USER_FIELD_PREFIX) &&
      ::UserField.exists?(field.split("_").last.to_i)
  end

  def allowed_profile_fields
    CustomWizard::Mapper.user_fields.select { |f| profile_excluded_fields.exclude?(f) } +
      profile_url_fields + ["avatar"]
  end

  def update_avatar(upload_id)
    user.create_user_avatar unless user.user_avatar
    user.user_avatar.custom_upload_id = upload_id
    user.uploaded_avatar_id = upload_id
    user.save!
    user.user_avatar.save!
  end

  def encode_query_param(param)
    Addressable::URI.encode_component(param, Addressable::URI::CharacterClasses::UNRESERVED)
  end

  def log_success(message, detail = nil)
    @log.push("success: #{message} - #{detail}")
    @result.success = true
  end

  def log_error(message, detail = nil)
    @log.push("error: #{message} - #{detail}")
    @result.success = false
  end

  def log_info(message, detail = nil)
    @log.push("info: #{message} - #{detail}")
  end

  def save_log
    username = user ? user.username : @wizard.actor_id

    CustomWizard::Log.create(@wizard.id, action["type"], username, @log.join("; "))
  end
end
