class StepsController < ApplicationController
  include ActionView::Helpers::TextHelper
  include ApplicationHelper
  include StepsActions
  include MarvinJsActions

  before_action :load_vars, only: %i(update destroy show toggle_step_state update_view_state
                                     update_asset_view_mode elements
                                     attachments upload_attachment duplicate)
  before_action :load_vars_nested, only: %i(create index list reorder list_protocol_steps add_protocol_steps)
  before_action :convert_table_contents_to_utf8, only: %i(create update)

  before_action :check_protocol_manage_permissions, only: %i(reorder add_protocol_steps)
  before_action :check_view_permissions, only: %i(show index list attachments elements list_protocol_steps)
  before_action :check_create_permissions, only: %i(create)
  before_action :check_manage_permissions, only: %i(update destroy
                                                    update_view_state update_asset_view_mode upload_attachment)
  before_action :check_complete_and_checkbox_permissions, only: %i(toggle_step_state)

  def index
    render json: @protocol.steps.in_order, each_serializer: StepSerializer, user: current_user
  end

  def list
    @steps = @protocol.steps.in_order
  end

  def elements
    render json: @step.step_orderable_elements.order(:position),
           each_serializer: StepOrderableElementSerializer,
           user: current_user
  end

  def attachments
    render json: @step.assets.preload(:preview_image_attachment, file_attachment: :blob,
                                      step: { protocol: { my_module: { experiment: :project, user_assignments: %i(user user_role) } } }),
           each_serializer: AssetSerializer,
           user: current_user,
           managable_step: can_manage_step?(@step)
  end

  def upload_attachment
    @step.transaction do
      @asset = @step.assets.create!(
        created_by: current_user,
        last_modified_by: current_user,
        team: @protocol.team,
        view_mode: @step.assets_view_mode
      )
      @asset.attach_file_version(params[:signed_blob_id])
      @asset.post_process_file

      default_message_items = {
        step: @step.id,
        step_position: { id: @step.id,
                         value_for: 'position_plus_one' }
      }

      if @protocol.in_module?
        log_activity(
          :task_step_file_added,
          @my_module.experiment.project,
          {
            file: @asset.file_name,
            my_module: @my_module.id
          }.merge(default_message_items)
        )
      else
        log_activity(
          :protocol_step_file_added,
          nil,
          {
            file: @asset.file_name,
            protocol: @protocol.id
          }.merge(default_message_items)
        )
      end
    end

    render json: @asset,
           serializer: AssetSerializer,
           user: current_user
  end

  def create
    @step = Step.new(
      name: t('protocols.steps.default_name'),
      completed: false,
      user: current_user,
      last_modified_by: current_user
    )

    @step = @protocol.insert_step(@step, params[:position])
    if @protocol.in_repository? && @protocol.errors.present?
      return render json: { error: @protocol.errors }, status: :unprocessable_entity
    end

    # Generate activity
    if @protocol.in_module?
      log_activity(:create_step, @my_module.experiment.project, { my_module: @my_module.id }.merge(step_message_items))
    else
      log_activity(:add_step_to_protocol_repository, nil, { protocol: @protocol.id }.merge(step_message_items))
    end
    render json: @step, serializer: StepSerializer, user: current_user
  end

  def show
    render json: {
      html: render_to_string(
        partial: 'steps/step',
        locals: { step: @step },
        formats: :html
      )
    }
  end

  def update
    if @step.update(step_params)
      # Generate activity
      if @protocol.in_module?
        log_activity(
          :edit_step, @my_module.experiment.project,
          { my_module: @my_module.id }.merge(step_message_items)
        )
      else
        log_activity(
          :edit_step_in_protocol_repository,
          nil,
          { protocol: @protocol.id }.merge(step_message_items)
        )
      end
      render json: @step, serializer: StepSerializer, user: current_user
    else
      render json: @protocol.errors.present? ? { errors: @protocol.errors } : {}, status: :unprocessable_entity
    end
  end

  def duplicate
    ActiveRecord::Base.transaction do
      position = @step.position
      @protocol.steps.where('position > ?', position).order(position: :desc).each do |step|
        step.update(position: step.position + 1)
      end
      new_step = @step.duplicate(@protocol, current_user, step_position: position + 1, step_name: @step.name + ' (1)')

      if @protocol.in_module?
        log_activity(
          :task_step_duplicated, @my_module.experiment.project,
          { my_module: @my_module.id }.merge(step_message_items)
        )
      else
        log_activity(
          :protocol_step_duplicated,
          nil,
          { protocol: @protocol.id }.merge(step_message_items)
        )
      end

      render json: new_step, serializer: StepSerializer, user: current_user
    end
  rescue ActiveRecord::RecordInvalid
    head :unprocessable_entity
  end

  def update_view_state
    view_state = @step.current_view_state(current_user)
    view_state.state['assets']['sort'] = params.require(:assets).require(:order)
    view_state.save! if view_state.changed?

    render json: {}, status: :ok
  end

  def update_asset_view_mode
    html = ''
    ActiveRecord::Base.transaction do
      @step.assets_view_mode = params[:assets_view_mode]
      @step.save!(touch: false)
      @step.assets.update_all(view_mode: @step.assets_view_mode)
    end
    render json: { view_mode: @step.assets_view_mode }, status: :ok
  rescue ActiveRecord::RecordInvalid => e
    Rails.logger.error(e.message)
    render json: { errors: e.message }, status: :unprocessable_entity
  end

  def destroy
    if @step.can_destroy?

      # Calculate space taken by this step
      team = @protocol.team
      previous_size = @step.space_taken

      # Generate activity
      if @protocol.in_module?
        log_activity(
          :destroy_step,
          @my_module.experiment.project,
          { my_module: @my_module.id }.merge(step_message_items)
        )
      else
        log_activity(:delete_step_in_protocol_repository, nil, { protocol: @protocol.id }.merge(step_message_items))
      end

      # Destroy the step
      @step.destroy

      # Release space taken by the step
      team.release_space(previous_size)
      team.save
    end

    render json: @step, serializer: StepSerializer, user: current_user
  end

  # Complete/uncomplete step
  def toggle_step_state
    @step.completed = params[:completed] == 'true'
    @step.last_modified_by = current_user

    if @step.save
      # Create activity
      if @step.saved_change_to_completed
        completed_steps = @protocol.steps.where(completed: true).count
        all_steps = @protocol.steps.count

        type_of = @step.completed ? :complete_step : :uncomplete_step
        # Toggling step state can only occur in
        # module protocols, so my_module is always
        # not nil; nonetheless, check if my_module is present
        if @protocol.in_module?
          log_activity(
            type_of,
            @protocol.my_module.experiment.project,
            {
              my_module: @my_module.id,
              num_completed: completed_steps.to_s,
              num_all: all_steps.to_s
            }.merge(step_message_items)
          )
        end
      end
      render json: @step, serializer: StepSerializer, user: current_user
    else
      render json: {}, status: :unprocessable_entity
    end
  end

  def reorder
    @protocol.with_lock do
      position_changed = false
      params[:step_positions].each do |id, position|
        step = @protocol.steps.find(id)
        if position != step.position
          position_changed = true
          step.update_column(:position, position)
        end
      end

      if position_changed
        if @protocol.in_module?
          log_activity(:task_steps_rearranged, @my_module.experiment.project, my_module: @my_module.id)
        else
          log_activity(:protocol_steps_rearranged, nil, protocol: @protocol.id)
        end
        @protocol.touch
      end
    end

    render json: {
      steps_order: @protocol.steps.order(:position).select(:id, :position)
    }
  end

  def list_protocol_steps
    steps = @protocol.steps

    if params[:query].present?
      steps = steps.where_attributes_like(
        ['steps.name'],
        params[:query]
      )
    end

    steps = steps.order(position: :asc).page(params[:page])
    render json: {
      paginated: true,
      next_page: steps.next_page,
      data: steps.map { |step| [step.id, step.name, { position: step.position }] }
    }
  end

  def add_protocol_steps
    Protocol.transaction do
      selected_protocol = Protocol.find_by(id: params[:selected_protocol])
      render_403 unless selected_protocol.present? && can_read_protocol_in_repository?(selected_protocol)

      steps = selected_protocol.steps.where(id: params[:steps]).order(position: :asc).map do |step|
        step.duplicate(@protocol, current_user, original_protocol: selected_protocol)
      end

      message_items = {
        protocol: selected_protocol.id,
        count: steps.count
      }

      if @protocol.in_module?
        message_items[:my_module] = @protocol.my_module.id
        log_activity(:task_steps_loaded_from_template, @my_module.experiment.project, message_items)
      else
        log_activity(:protocol_steps_loaded_from_template, nil, message_items)
      end

      render json: steps, each_serializer: StepSerializer, user: current_user
    end
  end

  private

  def load_vars
    @step = Step.find_by(id: params[:id])
    return render_404 unless @step

    @protocol = @step.protocol
    @chk_item = ChecklistItem.find_by(id: params[:checklistitem_id]) if params[:checklistitem_id]
    @my_module = @protocol.my_module if @protocol.in_module?
  end

  def load_vars_nested
    @protocol = Protocol.find_by(id: params[:protocol_id])

    return render_404 unless @protocol

    @my_module = @protocol.my_module if @protocol.in_module?
  end

  def convert_table_contents_to_utf8
    if params.include? :step and
      params[:step].include? :tables_attributes then
      params[:step][:tables_attributes].each do |k,v|
        params[:step][:tables_attributes][k][:contents] =
          v[:contents].encode(Encoding::UTF_8).force_encoding(Encoding::UTF_8)
      end
    end
  end

  def check_view_permissions
    render_403 unless can_read_protocol_in_module?(@protocol) || can_read_protocol_in_repository?(@protocol)
  end

  def check_protocol_manage_permissions
    render_403 unless can_manage_protocol_in_module?(@protocol) || can_manage_protocol_draft_in_repository?(@protocol)
  end

  def check_manage_permissions
    render_403 unless can_manage_step?(@step)
  end

  def check_create_permissions
    if @my_module
      render_403 unless can_manage_my_module_steps?(@my_module)
    else
      render_403 unless can_manage_protocol_draft_in_repository?(@protocol)
    end
  end

  def check_complete_and_checkbox_permissions
    render_403 unless can_complete_or_checkbox_step?(@protocol)
  end

  def step_params
    params.require(:step).permit(:name)
  end

  def log_activity(type_of, project = nil, message_items = {})
    Activities::CreateActivityService
      .call(activity_type: type_of,
            owner: current_user,
            subject: @protocol,
            team: @protocol.team,
            project: project,
            message_items: message_items)
  end

  def step_message_items
    {
      step: {
        id: @step.id,
        value_for: 'name'
      },
      step_position: {
        id: @step.id,
        value_for: 'position_plus_one'
      }
    }
  end
end
