# frozen_string_literal: true

module Api
  module V1
    # Controller for managing party-related operations in the API
    # @api public
    class PartiesController < Api::V1::ApiController
      include PartyAuthorizationConcern
      include PartyQueryingConcern
      include PartyPreviewConcern

      # Constants used for filtering validations.

      # Maximum number of characters allowed in a party
      MAX_CHARACTERS = 5

      # Maximum number of summons allowed in a party
      MAX_SUMMONS = 8

      # Maximum number of weapons allowed in a party
      MAX_WEAPONS = 13

      # Default minimum number of characters required for filtering
      DEFAULT_MIN_CHARACTERS = 3

      # Default minimum number of summons required for filtering
      DEFAULT_MIN_SUMMONS = 2

      # Default minimum number of weapons required for filtering
      DEFAULT_MIN_WEAPONS = 5

      # Default maximum clear time in seconds
      DEFAULT_MAX_CLEAR_TIME = 5400

      before_action :set_from_slug, except: %w[create destroy update index favorites grid_update]
      before_action :set, only: %w[update destroy grid_update]
      before_action :authorize_party!, only: %w[update destroy grid_update]

      # Primary CRUD Actions

      # Creates a new party with optional user association
      # @return [void]
      # Creates a new party.
      def create
        party = Party.new(party_params)
        party.user = current_user if current_user
        if party_params && party_params[:raid_id].present? && (raid = Raid.find_by(id: party_params[:raid_id]))
          party.extra = raid.group.extra
        end
        if party.save
          party.schedule_preview_generation if party.ready_for_preview?
          render json: PartyBlueprint.render(party, view: :created, root: :party), status: :created
        else
          render_validation_error_response(party)
        end
      end

      # Shows a specific party.
      def show
        return render_unauthorized_response if @party.private? && (!current_user || not_owner?)

        if @party
          render json: PartyBlueprint.render(@party, view: :full, root: :party)
        else
          render_not_found_response('project')
        end
      end

      # Updates an existing party.
      def update
        @party.attributes = party_params.except(:skill1_id, :skill2_id, :skill3_id)
        if party_params && party_params[:raid_id] && (raid = Raid.find_by(id: party_params[:raid_id]))
          @party.extra = raid.group.extra
        end
        if @party.save
          render json: PartyBlueprint.render(@party, view: :full, root: :party)
        else
          render_validation_error_response(@party)
        end
      end

      # Deletes a party.
      def destroy
        if @party.destroy
          head :no_content
        else
          render_unprocessable_entity_response(
            Api::V1::PartyDeletionFailedError.new(@party.errors.full_messages)
          )
        end
      end

      # Extended Party Actions

      # Creates a remixed copy of an existing party.
      def remix
        new_party = @party.amoeba_dup
        new_party.attributes = { user: current_user, name: remixed_name(@party.name), source_party: @party,
                                 remix: true }
        new_party.local_id = party_params[:local_id] if party_params
        if new_party.save
          new_party.schedule_preview_generation
          render json: PartyBlueprint.render(new_party, view: :remixed, root: :party), status: :created
        else
          render_validation_error_response(new_party)
        end
      end

      # Batch updates grid items (weapons, characters, summons) atomically.
      def grid_update
        operations = grid_update_params[:operations]
        options = grid_update_params[:options] || {}

        # Validate all operations first
        validation_errors = validate_grid_operations(operations)
        if validation_errors.any?
          return render_unprocessable_entity_response(
            Api::V1::GranblueError.new("Validation failed: #{validation_errors.join(', ')}")
          )
        end

        changes = []

        ActiveRecord::Base.transaction do
          operations.each do |operation|
            change = apply_grid_operation(operation)
            changes << change if change
          end

          # Compact character positions if needed
          compact_party_character_positions if options[:maintain_character_sequence]
        end

        render json: {
          party: PartyBlueprint.render_as_hash(@party.reload, view: :full),
          operations_applied: changes.count,
          changes: changes
        }, status: :ok
      rescue StandardError => e
        render_unprocessable_entity_response(
          Api::V1::GranblueError.new("Grid update failed: #{e.message}")
        )
      end

      # Syncs all linked grid items from their collection sources.
      #
      # POST /parties/:id/sync_all
      def sync_all
        @party = Party.find_by(id: params[:id])
        return render_not_found_response('party') unless @party
        return render_unauthorized_response unless authorized_to_edit?

        synced = { characters: 0, weapons: 0, summons: 0, artifacts: 0 }

        ActiveRecord::Base.transaction do
          @party.characters.where.not(collection_character_id: nil).each do |gc|
            gc.sync_from_collection!
            synced[:characters] += 1
          end

          @party.weapons.where.not(collection_weapon_id: nil).each do |gw|
            gw.sync_from_collection!
            synced[:weapons] += 1
          end

          @party.summons.where.not(collection_summon_id: nil).each do |gs|
            gs.sync_from_collection!
            synced[:summons] += 1
          end

          GridArtifact.joins(:grid_character)
                      .where(grid_characters: { party_id: @party.id })
                      .where.not(collection_artifact_id: nil)
                      .each do |ga|
            ga.sync_from_collection!
            synced[:artifacts] += 1
          end
        end

        render json: {
          party: PartyBlueprint.render_as_hash(@party.reload, view: :full),
          synced: synced
        }, status: :ok
      end

      # Lists parties based on query parameters.
      def index
        query = build_filtered_query(build_common_base_query)
        @parties = query.paginate(page: params[:page], per_page: page_size)

        # Preload current user's favorite party IDs to avoid N+1
        favorite_party_ids = current_user ? current_user.favorites.pluck(:party_id).to_set : Set.new

        render json: Api::V1::PartyBlueprint.render(
          @parties,
          view: :preview,
          root: :results,
          meta: pagination_meta(@parties),
          current_user: current_user,
          favorite_party_ids: favorite_party_ids
        )
      end

      # GET /api/v1/parties/favorites
      def favorites
        raise Api::V1::UnauthorizedError unless current_user

        base_query = build_common_base_query
                     .joins(:favorites)
                     .where(favorites: { user_id: current_user.id })
                     .distinct
        query = build_filtered_query(base_query)
        @parties = query.paginate(page: params[:page], per_page: page_size)

        # All parties in this list are favorites, but preload for consistency
        favorite_party_ids = current_user.favorites.pluck(:party_id).to_set

        render json: Api::V1::PartyBlueprint.render(
          @parties,
          view: :preview,
          root: :results,
          meta: pagination_meta(@parties),
          current_user: current_user,
          favorite_party_ids: favorite_party_ids
        )
      end

      # Preview Management

      # Serves the party's preview image
      # @return [void]
      # Serves the party's preview image.
      def preview
        party_preview(@party)
      end

      # Returns the current preview status of a party.
      def preview_status
        party = Party.find_by!(shortcode: params[:id])
        render json: { state: party.preview_state, generated_at: party.preview_generated_at,
                       ready_for_preview: party.ready_for_preview? }
      end

      # Forces regeneration of the party preview.
      def regenerate_preview
        party = Party.find_by!(shortcode: params[:id])
        return render_unauthorized_response unless current_user && party.user_id == current_user.id

        preview_service = PreviewService::Coordinator.new(party)
        if preview_service.force_regenerate
          render json: { status: 'Preview regeneration started' }
        else
          render json: { error: 'Preview regeneration failed' }, status: :unprocessable_entity
        end
      end

      private

      # Loads the party by its shortcode.
      def set_from_slug
        @party = Party.includes(
          :user, :job, { raid: :group },
          { characters: [:character, :awakening, :grid_artifact] },
          { weapons: {
            weapon: [:awakenings, :weapon_series],
            awakening: {},
            weapon_key1: {},
            weapon_key2: {},
            weapon_key3: {},
            ax_modifier1: {},
            ax_modifier2: {},
            befoulment_modifier: {}
          } },
          { summons: :summon },
          :guidebook1, :guidebook2, :guidebook3,
          :source_party, :remixes, :skill0, :skill1, :skill2, :skill3, :accessory
        ).find_by(shortcode: params[:id])
        render_not_found_response('party') unless @party
      end

      # Loads the party by its id.
      def set
        @party = Party.where('id = ?', params[:id]).first
      end

      # Sanitizes and permits party parameters.
      def party_params
        return unless params[:party].present?

        params.require(:party).permit(
          :user_id, :local_id, :edit_key, :extra, :name, :description, :raid_id, :job_id, :visibility,
          :accessory_id, :skill0_id, :skill1_id, :skill2_id, :skill3_id,
          :full_auto, :auto_guard, :auto_summon, :charge_attack, :clear_time, :button_count,
          :turn_count, :chain_count, :summon_count, :video_url, :guidebook1_id, :guidebook2_id, :guidebook3_id,
          characters_attributes: [:id, :party_id, :character_id, :position, :uncap_level,
                                  :transcendence_step, :perpetuity, :awakening_id, :awakening_level,
                                  { ring1: %i[modifier strength], ring2: %i[modifier strength], ring3: %i[modifier strength], ring4: %i[modifier strength],
                                    earring: %i[modifier strength] }],
          summons_attributes: %i[id party_id summon_id position main friend quick_summon uncap_level transcendence_step],
          weapons_attributes: %i[id party_id weapon_id position mainhand uncap_level transcendence_step element weapon_key1_id weapon_key2_id weapon_key3_id ax_modifier1_id ax_modifier2_id ax_strength1 ax_strength2 befoulment_modifier_id befoulment_strength exorcism_level awakening_id awakening_level]
        )
      end

      # Permits parameters for grid update operation.
      def grid_update_params
        params.permit(
          operations: %i[type entity id source_id target_id position container],
          options: %i[maintain_character_sequence validate_before_execute]
        )
      end

      # Validates grid operations before executing.
      def validate_grid_operations(operations)
        errors = []

        operations.each_with_index do |op, index|
          case op[:type]
          when 'move'
            errors << "Operation #{index}: missing id" unless op[:id].present?
            errors << "Operation #{index}: missing position" unless op[:position].present?
          when 'swap'
            errors << "Operation #{index}: missing source_id" unless op[:source_id].present?
            errors << "Operation #{index}: missing target_id" unless op[:target_id].present?
          when 'remove'
            errors << "Operation #{index}: missing id" unless op[:id].present?
          else
            errors << "Operation #{index}: unknown operation type #{op[:type]}"
          end

          unless %w[weapon character summon].include?(op[:entity])
            errors << "Operation #{index}: invalid entity type #{op[:entity]}"
          end
        end

        errors
      end

      # Applies a single grid operation.
      def apply_grid_operation(operation)
        case operation[:type]
        when 'move'
          apply_move_operation(operation)
        when 'swap'
          apply_swap_operation(operation)
        when 'remove'
          apply_remove_operation(operation)
        end
      end

      # Applies a move operation.
      def apply_move_operation(operation)
        model_class = grid_model_for_entity(operation[:entity])
        item = model_class.find_by(id: operation[:id], party_id: @party.id)

        return nil unless item

        old_position = item.position
        item.update!(position: operation[:position])

        {
          entity: operation[:entity],
          id: operation[:id],
          action: 'moved',
          from: old_position,
          to: operation[:position]
        }
      end

      # Applies a swap operation.
      def apply_swap_operation(operation)
        model_class = grid_model_for_entity(operation[:entity])
        source = model_class.find_by(id: operation[:source_id], party_id: @party.id)
        target = model_class.find_by(id: operation[:target_id], party_id: @party.id)

        return nil unless source && target

        source_pos = source.position
        target_pos = target.position

        # Use a temporary position to avoid conflicts
        source.update!(position: -999)
        target.update!(position: source_pos)
        source.update!(position: target_pos)

        {
          entity: operation[:entity],
          id: operation[:source_id],
          action: 'swapped',
          with: operation[:target_id]
        }
      end

      # Applies a remove operation.
      def apply_remove_operation(operation)
        model_class = grid_model_for_entity(operation[:entity])
        item = model_class.find_by(id: operation[:id], party_id: @party.id)

        return nil unless item

        item.destroy

        {
          entity: operation[:entity],
          id: operation[:id],
          action: 'removed'
        }
      end

      # Returns the model class for a given entity type.
      def grid_model_for_entity(entity)
        case entity
        when 'weapon'
          GridWeapon
        when 'character'
          GridCharacter
        when 'summon'
          GridSummon
        end
      end

      # Compacts character positions to maintain sequential filling.
      def compact_party_character_positions
        main_characters = @party.characters.where(position: 0..4).order(:position)

        main_characters.each_with_index do |char, index|
          char.update!(position: index) if char.position != index
        end
      end
    end
  end
end
