# frozen_string_literal: true module Api module V1 ## # ImportController is responsible for importing game data (e.g. deck data) # and creating a new Party along with associated records (job, characters, weapons, summons, etc.). # # The controller expects a JSON payload whose top-level key is "import". If not wrapped, # the controller will wrap the raw data automatically. # # @example Valid payload structure # { # "import": { # "deck": { "name": "My Party", ... }, # "pc": { "job": { "master": { "name": "Warrior" } }, ... } # } # } class ImportController < Api::V1::ApiController ELEMENT_MAPPING = { 0 => nil, 1 => 4, 2 => 2, 3 => 3, 4 => 1, 5 => 6, 6 => 5 }.freeze # GBF series_id to CharacterSeries slug mapping GBF_SERIES_TO_SLUG = { 1 => 'summer', 2 => 'yukata', 3 => 'valentine', 4 => 'halloween', 5 => 'holiday', 6 => 'zodiac', 7 => 'grand', 8 => 'fantasy', 9 => 'collab', 10 => 'eternal', 11 => 'evoker', 12 => 'saint', 13 => 'formal' }.freeze # GBF series_id to WeaponSeries slug mapping GBF_WEAPON_SERIES_TO_SLUG = { 1 => 'seraphic', 2 => 'grand', 3 => 'dark-opus', 4 => 'revenant', 5 => 'primal', 6 => 'beast', 7 => 'regalia', 8 => 'omega', 9 => 'olden-primal', 10 => 'hollowsky', 11 => 'xeno', 12 => 'rose', 13 => 'ultima', 14 => 'bahamut', 15 => 'epic', 16 => 'cosmos', 17 => 'superlative', 18 => 'vintage', 19 => 'class-champion', 20 => 'replica', 21 => 'relic', 22 => 'rusted', 23 => 'sephira', 24 => 'vyrmament', 25 => 'upgrader', 26 => 'astral', 27 => 'draconic', 28 => 'eternal-splendor', 29 => 'ancestral', 30 => 'new-world-foundation', 31 => 'ennead', 32 => 'militis', 33 => 'malice', 34 => 'menace', 35 => 'illustrious', 36 => 'proven', 37 => 'revans', 38 => 'world', 39 => 'exo', 40 => 'draconic-providence', 41 => 'celestial', 42 => 'omega-rebirth', 43 => 'collab', 44 => 'destroyer' }.freeze # GBF series_id to SummonSeries slug mapping GBF_SUMMON_SERIES_TO_SLUG = { 1 => 'providence', 2 => 'genesis', 3 => 'magna', 4 => 'optimus', 5 => 'demi-optimus', 6 => 'archangel', 7 => 'arcarum', 8 => 'epic', 9 => 'carbuncle', 10 => 'dynamis', 12 => 'cryptid', 13 => 'six-dragons', 14 => 'summer', 15 => 'yukata', 16 => 'holiday', 17 => 'collab', 18 => 'bellum', 19 => 'crest', 20 => 'robur' }.freeze before_action :ensure_admin_role, only: %i[weapons summons characters] ## # Processes an import request. # # It reads and parses the raw JSON, wraps the data under the "import" key if necessary, # transforms the deck data using BaseDeckTransformer, validates that the transformed data # contains required fields, and then creates a new Party record (and its associated objects) # inside a transaction. # # @return [void] Renders JSON response with a party shortcode or an error message. def create Rails.logger.info '[IMPORT] Checking input...' body = parse_request_body return unless body raw_params = body['import'] unless raw_params.is_a?(Hash) Rails.logger.error "[IMPORT] 'import' key is missing or not a hash." return render json: { error: 'Invalid JSON data' }, status: :unprocessable_content end unless raw_params['deck'].is_a?(Hash) && raw_params['deck'].key?('pc') && raw_params['deck'].key?('npc') Rails.logger.error '[IMPORT] Deck data incomplete or missing.' return render json: { error: 'Invalid deck data' }, status: :unprocessable_content end Rails.logger.info '[IMPORT] Starting import...' return if performed? # Rendered an error response already party = Party.create(user: current_user) deck_data = raw_params['import'] process_data(party, deck_data) render json: { shortcode: party.shortcode }, status: :created rescue StandardError => e render json: { error: e.message }, status: :unprocessable_content end def weapons Rails.logger.info '[IMPORT] Checking weapon gamedata input...' body = parse_request_body return unless body weapon = Weapon.find_by(granblue_id: body['id']) unless weapon Rails.logger.error "[IMPORT] Weapon not found with ID: #{body['id']}" return render json: { error: 'Weapon not found' }, status: :not_found end lang = params[:lang] unless %w[en jp].include?(lang) Rails.logger.error "[IMPORT] Invalid language: #{lang}" return render json: { error: 'Invalid language' }, status: :unprocessable_content end begin weapon.update!( "game_raw_#{lang}" => body.to_json ) # Parse series_id and assign WeaponSeries series_id = body['series_id'] || body.dig('master', 'series_id') if series_id slug = GBF_WEAPON_SERIES_TO_SLUG[series_id.to_i] if slug series_record = WeaponSeries.find_by(slug: slug) if series_record && weapon.weapon_series != series_record weapon.update!(weapon_series: series_record) Rails.logger.info "[IMPORT] Set series '#{slug}' for weapon #{weapon.granblue_id}" end end end render json: { message: 'Weapon gamedata updated successfully' }, status: :ok rescue StandardError => e Rails.logger.error "[IMPORT] Failed to update weapon gamedata: #{e.message}" render json: { error: e.message }, status: :unprocessable_content end end def summons Rails.logger.info '[IMPORT] Checking summon gamedata input...' body = parse_request_body return unless body summon = Summon.find_by(granblue_id: body['id']) unless summon Rails.logger.error "[IMPORT] Summon not found with ID: #{body['id']}" return render json: { error: 'Summon not found' }, status: :not_found end lang = params[:lang] unless %w[en jp].include?(lang) Rails.logger.error "[IMPORT] Invalid language: #{lang}" return render json: { error: 'Invalid language' }, status: :unprocessable_content end begin summon.update!( "game_raw_#{lang}" => body.to_json ) # Parse series_id and assign SummonSeries series_id = body['series_id'] || body.dig('master', 'series_id') if series_id slug = GBF_SUMMON_SERIES_TO_SLUG[series_id.to_i] if slug series_record = SummonSeries.find_by(slug: slug) if series_record && summon.summon_series != series_record summon.update!(summon_series: series_record) Rails.logger.info "[IMPORT] Set series '#{slug}' for summon #{summon.granblue_id}" end end end render json: { message: 'Summon gamedata updated successfully' }, status: :ok rescue StandardError => e Rails.logger.error "[IMPORT] Failed to update summon gamedata: #{e.message}" render json: { error: e.message }, status: :unprocessable_content end end ## # Updates character gamedata from JSON blob. # # @return [void] Renders JSON response with success or error message. def characters Rails.logger.info '[IMPORT] Checking character gamedata input...' body = parse_request_body return unless body character = Character.find_by(granblue_id: body['id']) unless character Rails.logger.error "[IMPORT] Character not found with ID: #{body['id']}" return render json: { error: 'Character not found' }, status: :not_found end lang = params[:lang] unless %w[en jp].include?(lang) Rails.logger.error "[IMPORT] Invalid language: #{lang}" return render json: { error: 'Invalid language' }, status: :unprocessable_content end begin character.update!( "game_raw_#{lang}" => body.to_json ) # Parse series_id and create CharacterSeriesMembership series_id = body['series_id'] || body.dig('master', 'series_id') if series_id slug = GBF_SERIES_TO_SLUG[series_id.to_i] if slug series_record = CharacterSeries.find_by(slug: slug) if series_record && !character.character_series_records.include?(series_record) character.character_series_memberships.create!(character_series: series_record) Rails.logger.info "[IMPORT] Added series '#{slug}' to character #{character.granblue_id}" end end end render json: { message: 'Character gamedata updated successfully' }, status: :ok rescue StandardError => e Rails.logger.error "[IMPORT] Failed to update character gamedata: #{e.message}" render json: { error: e.message }, status: :unprocessable_content end end private ## # Ensures the current user has admin role (role 9). # Renders an error if the user is not an admin. # # @return [void] def ensure_admin_role return if current_user&.role == 9 Rails.logger.error "[IMPORT] Unauthorized access attempt by user #{current_user&.id}" render json: { error: 'Unauthorized' }, status: :unauthorized end ## # Reads and parses the raw JSON request body. # # @return [Hash] Parsed JSON data. # @raise [JSON::ParserError] If the JSON is invalid. def parse_request_body raw_body = request.raw_post JSON.parse(raw_body) rescue JSON::ParserError => e Rails.logger.error "[IMPORT] Invalid JSON: #{e.message}" render json: { error: 'Invalid JSON data' }, status: :bad_request and return end ## # Ensures that the provided data is wrapped under an "import" key. # # @param data [Hash] The parsed JSON data. # @return [Hash] Data wrapped under the "import" key. def wrap_import_data(data) data.key?('import') ? data : { 'import' => data } end ## # Processes the deck data using processors. # # @param party [Party] The party to insert data into # @param data [Hash] The wrapped data. # @return [Hash] The transformed deck data. def process_data(party, data) Rails.logger.info '[IMPORT] Transforming deck data' Processors::JobProcessor.new(party, data).process Processors::CharacterProcessor.new(party, data).process Processors::SummonProcessor.new(party, data).process Processors::WeaponProcessor.new(party, data).process end end end end