class IDRAC::Firmware

def check_updates(catalog_path = nil)

def check_updates(catalog_path = nil)
  # Ensure we have a client for system inventory
  raise Error, "Client is required for checking updates" unless client
  
  # Download catalog if not provided
  catalog_path ||= download_catalog
  
  # Get system inventory
  inventory = get_system_inventory
  
  # Create a FirmwareCatalog instance
  catalog = FirmwareCatalog.new(catalog_path)
  
  # Extract system information
  system_model = inventory[:system][:model]
  service_tag = inventory[:system][:service_tag]
  
  puts "Checking updates for system with service tag: #{service_tag}".light_cyan
  puts "Searching for updates for model: #{system_model}".light_cyan
  
  # Find system models in the catalog
  models = catalog.find_system_models(system_model)
  
  if models.empty?
    puts "No matching system model found in catalog".yellow
    return []
  end
  
  # Use the first matching model
  model = models.first
  puts "Found system IDs for #{model[:name]}: #{model[:id]}".green
  
  # Find updates for this system
  catalog_updates = catalog.find_updates_for_system(model[:id])
  puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}".green
  
  # Compare current firmware with available updates
  updates = []
  
  # Print header for firmware comparison table
  puts "\nFirmware Version Comparison:".green.bold
  puts "=" * 100
  puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
  puts "-" * 100
  
  # Track components we've already displayed to avoid duplicates
  displayed_components = Set.new
  
  # First show current firmware with available updates
  inventory[:firmware].each do |fw|
    # Make sure firmware name is not nil
    firmware_name = fw[:name] || ""
    
    # Skip if we've already displayed this component
    next if displayed_components.include?(firmware_name.downcase)
    displayed_components.add(firmware_name.downcase)
    
    # Extract key identifiers from the firmware name
    identifiers = extract_identifiers(firmware_name)
    
    # Try to find a matching update
    matching_updates = catalog_updates.select do |update|
      update_name = update[:name] || ""
      
      # Check if any of our identifiers match the update name
      identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
      # Or if the update name contains the firmware name
      update_name.downcase.include?(firmware_name.downcase) ||
      # Or if the firmware name contains the update name
      firmware_name.downcase.include?(update_name.downcase)
    end
    
    if matching_updates.any?
      # Use the first matching update
      update = matching_updates.first
      
      # Check if version is newer
      needs_update = catalog.compare_versions(fw[:version], update[:version])
      
      # Add to updates list if needed
      if needs_update && fw[:updateable]
        updates << {
          name: fw[:name],
          current_version: fw[:version],
          available_version: update[:version],
          path: update[:path],
          component_type: update[:component_type],
          category: update[:category],
          download_url: update[:download_url]
        }
        
        # Print row with update available
        puts "%-30s %-20s %-20s %-10s %-15s %s" % [
          fw[:name].to_s[0..29],
          fw[:version],
          update[:version],
          fw[:updateable] ? "Yes".light_green : "No".light_red,
          update[:category] || "N/A",
          "UPDATE AVAILABLE".light_green.bold
        ]
      else
        # Print row with no update needed
        status = if !needs_update
                   "Current".light_blue
                 elsif !fw[:updateable]
                   "Not updateable".light_red
                 else
                   "No update needed".light_yellow
                 end
        
        puts "%-30s %-20s %-20s %-10s %-15s %s" % [
          fw[:name].to_s[0..29],
          fw[:version],
          update[:version] || "N/A",
          fw[:updateable] ? "Yes".light_green : "No".light_red,
          update[:category] || "N/A",
          status
        ]
      end
    else
      # No matching update found
      puts "%-30s %-20s %-20s %-10s %-15s %s" % [
        fw[:name].to_s[0..29],
        fw[:version],
        "N/A",
        fw[:updateable] ? "Yes".light_green : "No".light_red,
        "N/A",
        "No update available".light_yellow
      ]
    end
  end
  
  # Then show available updates that don't match any current firmware
  catalog_updates.each do |update|
    update_name = update[:name] || ""
    
    # Skip if we've already displayed this component
    next if displayed_components.include?(update_name.downcase)
    displayed_components.add(update_name.downcase)
    
    # Skip if this update was already matched to a current firmware
    next if inventory[:firmware].any? do |fw|
      firmware_name = fw[:name] || ""
      identifiers = extract_identifiers(firmware_name)
      
      identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
      update_name.downcase.include?(firmware_name.downcase) ||
      firmware_name.downcase.include?(update_name.downcase)
    end
    
    puts "%-30s %-20s %-20s %-10s %-15s %s" % [
      update_name.to_s[0..29],
      "Not Installed".light_red,
      update[:version] || "Unknown",
      "N/A",
      update[:category] || "N/A",
      "NEW COMPONENT".light_blue
    ]
  end
  
  puts "=" * 100
  
  updates
end

def download_catalog(output_dir = nil)

def download_catalog(output_dir = nil)
  # Use the new FirmwareCatalog class
  catalog = FirmwareCatalog.new
  catalog.download(output_dir)
end

def download_firmware(update)

def download_firmware(update)
  return false unless update && update[:download_url]
  
  begin
    # Create a temporary directory for the download
    temp_dir = Dir.mktmpdir
    
    # Extract the filename from the path
    filename = File.basename(update[:path])
    local_path = File.join(temp_dir, filename)
    
    puts "Downloading firmware from #{update[:download_url]}".light_cyan
    puts "Saving to #{local_path}".light_cyan
    
    # Download the file
    uri = URI.parse(update[:download_url])
    
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
      request = Net::HTTP::Get.new(uri)
      
      http.request(request) do |response|
        if response.code == "200"
          File.open(local_path, 'wb') do |file|
            response.read_body do |chunk|
              file.write(chunk)
            end
          end
          
          puts "Download completed successfully".green
          return local_path
        else
          puts "Failed to download firmware: #{response.code} #{response.message}".red
          return false
        end
      end
    end
  rescue => e
    puts "Error downloading firmware: #{e.message}".red.bold
    return false
  end
end

def extract_identifiers(name)

Helper method to extract identifiers from component names
def extract_identifiers(name)
  return [] unless name
  
  identifiers = []
  
  # Extract model numbers like X520, I350, etc.
  model_matches = name.scan(/[IX]\d{3,4}/)
  identifiers.concat(model_matches)
  
  # Extract PERC model like H730
  perc_matches = name.scan(/[HP]\d{3,4}/)
  identifiers.concat(perc_matches)
  
  # Extract other common identifiers
  if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
    identifiers << "NIC"
  end
  
  if name.include?("PERC") || name.include?("RAID")
    identifiers << "PERC"
    # Extract PERC model like H730
    perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
    identifiers << perc_match[1] if perc_match
  end
  
  if name.include?("BIOS")
    identifiers << "BIOS"
  end
  
  if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
    identifiers << "iDRAC"
  end
  
  if name.include?("Power Supply") || name.include?("PSU")
    identifiers << "PSU"
  end
  
  if name.include?("Lifecycle Controller")
    identifiers << "LC"
  end
  
  if name.include?("CPLD")
    identifiers << "CPLD"
  end
  
  identifiers
end

def get_job_status(job_id)

def get_job_status(job_id)
  response = client.authenticated_request(
    :get,
    "/redfish/v1/TaskService/Tasks/#{job_id}"
  )
  
  # Status 202 means the request was accepted but still processing
  # This is normal for jobs that are in progress
  if response.status == 202 || response.status == 200
    begin
      data = JSON.parse(response.body)
      
      # Extract job state and percent complete
      job_state = data.dig('Oem', 'Dell', 'JobState') || data['TaskState']
      percent_complete = data.dig('Oem', 'Dell', 'PercentComplete') || data['PercentComplete']
      
      # Format the percent complete for display
      percent_str = percent_complete.nil? ? "unknown" : "#{percent_complete}%"
      
      puts "Job #{job_id} status: #{job_state} (#{percent_str} complete)".light_cyan
      
      return {
        id: job_id,
        state: job_state,
        percent_complete: percent_complete,
        status: data['TaskStatus'],
        message: data.dig('Oem', 'Dell', 'Message') || (data['Messages'].first && data['Messages'].first['Message']),
        raw_data: data
      }
    rescue JSON::ParserError => e
      puts "Error parsing job status response: #{e.message}".red.bold
      raise Error, "Failed to parse job status response: #{e.message}"
    end
  else
    puts "Failed to get job status with status #{response.status}: #{response.body}".red
    raise Error, "Failed to get job status with status #{response.status}"
  end
end

def get_power_state

def get_power_state
  # Ensure we have a client
  raise Error, "Client is required for power management" unless client
  
  # Login to iDRAC if needed
  client.login unless client.instance_variable_get(:@session_id)
  
  # Get system information
  response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
  
  if response.status == 200
    system_data = JSON.parse(response.body)
    return system_data["PowerState"]
  else
    raise Error, "Failed to get power state. Status code: #{response.status}"
  end
end

def get_system_inventory

def get_system_inventory
  # Ensure we have a client
  raise Error, "Client is required for system inventory" unless client
  
  puts "Retrieving system inventory..."
  
  # Get basic system information
  system_uri = URI.parse("#{client.base_url}/redfish/v1/Systems/System.Embedded.1")
  system_response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
  
  if system_response.status != 200
    raise Error, "Failed to get system information: #{system_response.status}"
  end
  
  system_data = JSON.parse(system_response.body)
  
  # Get firmware inventory
  firmware_uri = URI.parse("#{client.base_url}/redfish/v1/UpdateService/FirmwareInventory")
  firmware_response = client.authenticated_request(:get, "/redfish/v1/UpdateService/FirmwareInventory")
  
  if firmware_response.status != 200
    raise Error, "Failed to get firmware inventory: #{firmware_response.status}"
  end
  
  firmware_data = JSON.parse(firmware_response.body)
  
  # Get detailed firmware information for each component
  firmware_inventory = []
  
  if firmware_data['Members'] && firmware_data['Members'].is_a?(Array)
    firmware_data['Members'].each do |member|
      if member['@odata.id']
        component_uri = member['@odata.id']
        component_response = client.authenticated_request(:get, component_uri)
        
        if component_response.status == 200
          component_data = JSON.parse(component_response.body)
          firmware_inventory << {
            name: component_data['Name'],
            id: component_data['Id'],
            version: component_data['Version'],
            updateable: component_data['Updateable'] || false,
            status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
          }
        end
      end
    end
  end
  
  {
    system: {
      model: system_data['Model'],
      manufacturer: system_data['Manufacturer'],
      serial_number: system_data['SerialNumber'],
      part_number: system_data['PartNumber'],
      bios_version: system_data['BiosVersion'],
      service_tag: system_data['SKU']
    },
    firmware: firmware_inventory
  }
end

def initialize(client)

def initialize(client)
  @client = client
end

def interactive_update(catalog_path = nil, selected_updates = nil)

def interactive_update(catalog_path = nil, selected_updates = nil)
  # Check if updates are available
  updates = selected_updates || check_updates(catalog_path)
  
  if updates.empty?
    puts "No updates available for your system.".yellow
    return
  end
  
  # Display available updates
  puts "\nAvailable Updates:".green.bold
  updates.each_with_index do |update, index|
    puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}".light_cyan
  end
  
  # If no specific updates were selected, ask the user which ones to install
  if selected_updates.nil?
    puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):".light_yellow
    input = STDIN.gets.chomp
    
    if input.downcase == 'q'
      puts "Update cancelled.".yellow
      return
    elsif input.downcase == 'all'
      selected_updates = updates
    else
      begin
        index = input.to_i - 1
        if index >= 0 && index < updates.length
          selected_updates = [updates[index]]
        else
          puts "Invalid selection. Please enter a number between 1 and #{updates.length}.".red
          return
        end
      rescue
        puts "Invalid input. Please enter a number, 'all', or 'q'.".red
        return
      end
    end
  end
  
  # Process each selected update
  selected_updates.each do |update|
    puts "\nDownloading #{update[:name]} version #{update[:available_version]}...".light_cyan
    
    begin
      # Download the firmware
      firmware_file = download_firmware(update)
      
      if firmware_file
        puts "Installing #{update[:name]} version #{update[:available_version]}...".light_cyan
        
        begin
          # Upload and install the firmware
          job_id = upload_firmware(firmware_file)
          
          if job_id
            puts "Firmware update job created with ID: #{job_id}".green
            
            # Wait for the job to complete
            success = wait_for_job_completion(job_id, 1800) # 30 minutes timeout
            
            if success
              puts "Successfully updated #{update[:name]} to version #{update[:available_version]}".green.bold
            else
              puts "Failed to update #{update[:name]}. Check the iDRAC web interface for more details.".red
              puts "You may need to wait for any existing jobs to complete before trying again.".yellow
            end
          else
            puts "Failed to create update job for #{update[:name]}".red
          end
        rescue IDRAC::Error => e
          if e.message.include?("already in progress")
            puts "Error: A firmware update is already in progress.".red.bold
            puts "Please wait for the current update to complete before starting another.".yellow
            puts "You can check the status in the iDRAC web interface under Maintenance > System Update.".light_cyan
          elsif e.message.include?("job ID not found") || e.message.include?("Failed to get job status")
            puts "Error: Could not monitor the update job.".red.bold
            puts "The update may still be in progress. Check the iDRAC web interface for status.".yellow
            puts "This can happen if the iDRAC is busy processing the update request.".light_cyan
          else
            puts "Error during firmware update: #{e.message}".red.bold
          end
          
          # If we encounter an error with one update, ask if the user wants to continue with others
          if selected_updates.length > 1 && update != selected_updates.last
            puts "\nDo you want to continue with the remaining updates? (y/n)".light_yellow
            continue = STDIN.gets.chomp.downcase
            break unless continue == 'y'
          end
        end
      else
        puts "Failed to download firmware for #{update[:name]}".red
      end
    rescue => e
      puts "Error processing update for #{update[:name]}: #{e.message}".red.bold
      
      # If we encounter an error with one update, ask if the user wants to continue with others
      if selected_updates.length > 1 && update != selected_updates.last
        puts "\nDo you want to continue with the remaining updates? (y/n)".light_yellow
        continue = STDIN.gets.chomp.downcase
        break unless continue == 'y'
      end
    ensure
      # Clean up temporary files
      FileUtils.rm_f(firmware_file) if firmware_file && File.exist?(firmware_file)
    end
  end
end

def update(firmware_path, options = {})

def update(firmware_path, options = {})
  # Validate firmware file exists
  unless File.exist?(firmware_path)
    raise Error, "Firmware file not found: #{firmware_path}"
  end
  # Ensure we have a client
  raise Error, "Client is required for firmware update" unless client
  # Login to iDRAC
  client.login unless client.instance_variable_get(:@session_id)
  # Upload firmware file
  job_id = upload_firmware(firmware_path)
  
  # Check if we should wait for the update to complete
  if options[:wait]
    wait_for_job_completion(job_id, options[:timeout] || 3600)
  end
  job_id
end

def upload_firmware(firmware_path)

def upload_firmware(firmware_path)
  puts "Uploading firmware file: #{firmware_path}".light_cyan
  
  begin
    # First, get the HttpPushUri from the UpdateService
    response = client.authenticated_request(
      :get,
      "/redfish/v1/UpdateService"
    )
    
    if response.status != 200
      puts "Failed to get UpdateService information: #{response.status}".red
      raise Error, "Failed to get UpdateService information: #{response.status}"
    end
    
    update_service = JSON.parse(response.body)
    http_push_uri = update_service['HttpPushUri']
    
    if http_push_uri.nil?
      puts "HttpPushUri not found in UpdateService".red
      raise Error, "HttpPushUri not found in UpdateService"
    end
    
    puts "Found HttpPushUri: #{http_push_uri}".light_cyan
    
    # Get the ETag for the firmware inventory
    etag_response = client.authenticated_request(
      :get,
      http_push_uri
    )
    
    if etag_response.status != 200
      puts "Failed to get ETag: #{etag_response.status}".red
      raise Error, "Failed to get ETag: #{etag_response.status}"
    end
    
    etag = etag_response.headers['ETag']
    
    if etag.nil?
      puts "ETag not found in response headers".yellow
      # Some iDRACs don't require ETag, so we'll continue
    else
      puts "Got ETag: #{etag}".light_cyan
    end
    
    # Upload the firmware file
    file_content = File.read(firmware_path)
    
    headers = {
      'Content-Type' => 'multipart/form-data',
      'If-Match' => etag
    }
    
    # Create a temp file for multipart upload
    upload_io = Faraday::UploadIO.new(firmware_path, 'application/octet-stream')
    payload = { :file => upload_io }
    
    upload_response = client.authenticated_request(
      :post,
      http_push_uri,
      {
        headers: headers,
        body: payload,
      }
    )
    
    if upload_response.status != 201 && upload_response.status != 200
      puts "Failed to upload firmware: #{upload_response.status} - #{upload_response.body}".red
      
      if upload_response.body.include?("already in progress")
        raise Error, "A deployment or update operation is already in progress. Please wait for it to complete before attempting another update."
      else
        raise Error, "Failed to upload firmware: #{upload_response.status} - #{upload_response.body}"
      end
    end
    
    # Extract the firmware ID from the response
    begin
      upload_data = JSON.parse(upload_response.body)
      firmware_id = upload_data['Id'] || upload_data['@odata.id']&.split('/')&.last
      
      if firmware_id.nil?
        # Try to extract from the Location header
        location = upload_response.headers['Location']
        firmware_id = location&.split('/')&.last
      end
      
      if firmware_id.nil?
        puts "Warning: Could not extract firmware ID from response".yellow
        puts "Response body: #{upload_response.body}"
        # We'll try to continue with the SimpleUpdate action anyway
      else
        puts "Firmware file uploaded successfully with ID: #{firmware_id}".green
      end
    rescue JSON::ParserError => e
      puts "Warning: Could not parse upload response: #{e.message}".yellow
      puts "Response body: #{upload_response.body}"
      # We'll try to continue with the SimpleUpdate action anyway
    end
    
    # Now initiate the firmware update using SimpleUpdate action
    puts "Initiating firmware update using SimpleUpdate...".light_cyan
    
    # Construct the image URI
    image_uri = nil
    
    if firmware_id
      image_uri = "#{http_push_uri}/#{firmware_id}"
    else
      # If we couldn't extract the firmware ID, try using the Location header
      image_uri = upload_response.headers['Location']
    end
    
    # If we still don't have an image URI, try to use the HTTP push URI as a fallback
    if image_uri.nil?
      puts "Warning: Could not determine image URI, using HTTP push URI as fallback".yellow
      image_uri = http_push_uri
    end
    
    puts "Using ImageURI: #{image_uri}".light_cyan
    
    # Initiate the SimpleUpdate action
    simple_update_payload = {
      "ImageURI" => image_uri,
      "TransferProtocol" => "HTTP"
    }
    
    update_response = client.authenticated_request(
      :post,
      "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
      {
        headers: { 'Content-Type' => 'application/json' },
        body: simple_update_payload.to_json
      }
    )
    
    if update_response.status != 202 && update_response.status != 200
      puts "Failed to initiate firmware update: #{update_response.status} - #{update_response.body}".red
      raise Error, "Failed to initiate firmware update: #{update_response.status} - #{update_response.body}"
    end
    
    # Extract the job ID from the response
    job_id = nil
    
    # Try to extract from the response body first
    begin
      update_data = JSON.parse(update_response.body)
      job_id = update_data['Id'] || update_data['JobID']
    rescue JSON::ParserError
      # If we can't parse the body, that's okay, we'll try other methods
    end
    
    # If we couldn't get the job ID from the body, try the Location header
    if job_id.nil?
      location = update_response.headers['Location']
      job_id = location&.split('/')&.last
    end
    
    # If we still don't have a job ID, try the response headers
    if job_id.nil?
      # Some iDRACs return the job ID in a custom header
      update_response.headers.each do |key, value|
        if key.downcase.include?('job') && value.is_a?(String) && value.match?(/JID_\d+/)
          job_id = value
          break
        end
      end
    end
    
    # If we still don't have a job ID, check for any JID_ pattern in the response body
    if job_id.nil? && update_response.body.is_a?(String)
      match = update_response.body.match(/JID_\d+/)
      job_id = match[0] if match
    end
    
    # If we still don't have a job ID, check the task service for recent jobs
    if job_id.nil?
      puts "Could not extract job ID from response, checking task service for recent jobs...".yellow
      
      tasks_response = client.authenticated_request(
        :get,
        "/redfish/v1/TaskService/Tasks"
      )
      
      if tasks_response.status == 200
        begin
          tasks_data = JSON.parse(tasks_response.body)
          
          if tasks_data['Members'] && tasks_data['Members'].any?
            # Get the most recent task
            most_recent_task = tasks_data['Members'].first
            task_id = most_recent_task['@odata.id']&.split('/')&.last
            
            if task_id && task_id.match?(/JID_\d+/)
              job_id = task_id
              puts "Found recent job ID: #{job_id}".light_cyan
            end
          end
        rescue JSON::ParserError
          # If we can't parse the tasks response, we'll have to give up
        end
      end
    end
    
    if job_id.nil?
      puts "Could not extract job ID from response".red
      raise Error, "Could not extract job ID from response"
    end
    
    puts "Firmware update job created with ID: #{job_id}".green
    return job_id
  rescue => e
    puts "Error during firmware upload: #{e.message}".red.bold
    raise Error, "Error during firmware upload: #{e.message}"
  end
end

def wait_for_job_completion(job_id, timeout)

def wait_for_job_completion(job_id, timeout)
  puts "Waiting for firmware update job #{job_id} to complete...".light_cyan
  
  start_time = Time.now
  last_percent = -1
  
  while Time.now - start_time < timeout
    begin
      status = get_job_status(job_id)
      
      # Only show percentage updates when they change
      if status[:percent_complete] && status[:percent_complete] != last_percent
        puts "Job progress: #{status[:percent_complete]}% complete".light_cyan
        last_percent = status[:percent_complete]
      end
      
      case status[:state]
      when 'Completed'
        puts "Firmware update completed successfully".green
        return true
      when 'Failed', 'CompletedWithErrors'
        message = status[:message] || "Unknown error"
        puts "Firmware update failed: #{message}".red.bold
        return false
      when 'Stopped'
        puts "Firmware update stopped".yellow
        return false
      when 'New', 'Starting', 'Running', 'Pending', 'Scheduled', 'Downloaded', 'Downloading', 'Staged'
        # Job still in progress, continue waiting
        sleep 10
      else
        puts "Unknown job status: #{status[:state]}".yellow
        sleep 10
      end
    rescue => e
      puts "Error checking job status: #{e.message}".red
      puts "Will retry in 15 seconds...".yellow
      sleep 15
    end
  end
  
  puts "Timeout waiting for firmware update to complete".red.bold
  false
end