# frozen_string_literal: true

require 'base64'
require 'net/http'

# use #java_release_targets to access this list
JAVA_RELEASE_TARGETS = %w[
  //java/src/org/openqa/selenium/chrome:chrome.publish
  //java/src/org/openqa/selenium/chromium:chromium.publish
  //java/src/org/openqa/selenium/devtools/v149:v149.publish
  //java/src/org/openqa/selenium/devtools/v150:v150.publish
  //java/src/org/openqa/selenium/devtools/v148:v148.publish
  //java/src/org/openqa/selenium/devtools/latest:latest.publish
  //java/src/org/openqa/selenium/edge:edge.publish
  //java/src/org/openqa/selenium/firefox:firefox.publish
  //java/src/org/openqa/selenium/grid/node/kubernetes:kubernetes.publish
  //java/src/org/openqa/selenium/grid/sessionmap/jdbc:jdbc.publish
  //java/src/org/openqa/selenium/grid/sessionmap/redis:redis.publish
  //java/src/org/openqa/selenium/grid:bom-dependencies.publish
  //java/src/org/openqa/selenium/grid:bom.publish
  //java/src/org/openqa/selenium/grid:grid.publish
  //java/src/org/openqa/selenium/ie:ie.publish
  //java/src/org/openqa/selenium/json:json.publish
  //java/src/org/openqa/selenium/manager:manager.publish
  //java/src/org/openqa/selenium/os:os.publish
  //java/src/org/openqa/selenium/remote/http:http.publish
  //java/src/org/openqa/selenium/remote:remote.publish
  //java/src/org/openqa/selenium/safari:safari.publish
  //java/src/org/openqa/selenium/support:support.publish
  //java/src/org/openqa/selenium:client-combined.publish
  //java/src/org/openqa/selenium:core.publish
].freeze

def java_version
  File.foreach('java/version.bzl') do |line|
    return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION')
  end
end

def java_release_targets
  unless @targets_verified
    verify_java_release_targets
    @targets_verified = true
  end

  JAVA_RELEASE_TARGETS
end

def verify_java_release_targets
  query = 'kind(maven_publish, set(//java/... //third_party/...))'
  current_targets = []

  Bazel.execute('query', [], query) do |output|
    current_targets = output.lines.map(&:strip).reject(&:empty?).select { |line| line.start_with?('//') }
  end

  obsolete_targets = JAVA_RELEASE_TARGETS - current_targets
  unlisted_targets = current_targets - JAVA_RELEASE_TARGETS

  return if obsolete_targets.empty? && unlisted_targets.empty?

  error_message = 'Java release targets are out of sync with Bazel query results.'

  unless obsolete_targets.empty?
    error_message += "\nObsolete targets (in list but not in Bazel): #{obsolete_targets.join(', ')}"
  end

  unless unlisted_targets.empty?
    error_message += "\nMissing targets (in Bazel but not in list): #{unlisted_targets.join(', ')}"
  end

  raise error_message
end

def read_m2_user_pass
  settings_path = File.join(Dir.home, '.m2', 'settings.xml')
  unless File.exist?(settings_path)
    warn "Maven settings file not found at #{settings_path}"
    return
  end

  puts 'Maven environment variables not set, inspecting ~/.m2/settings.xml.'
  settings = File.read(settings_path)
  found_section = false
  settings.each_line do |line|
    if !found_section
      found_section = line.include? '<id>central</id>'
    elsif line.include?('<username>')
      ENV['MAVEN_USER'] = line[%r{<username>(.*?)</username>}, 1]
    elsif line.include?('<password>')
      ENV['MAVEN_PASSWORD'] = line[%r{<password>(.*?)</password>}, 1]
    end
    break if ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER']
  end
end

def sonatype_auth_token
  read_m2_user_pass unless ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER']
  Base64.strict_encode64("#{ENV.fetch('MAVEN_USER')}:#{ENV.fetch('MAVEN_PASSWORD')}")
end

def trigger_sonatype_publish(token)
  puts 'Triggering Sonatype upload with automatic publishing...'
  uri = URI('https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/org.seleniumhq?publishing_type=automatic')

  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Basic #{token}"
  req['Accept'] = '*/*'
  req['Content-Length'] = '0'

  begin
    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true,
                                                  open_timeout: 10, read_timeout: 180) do |http|
      http.request(req)
    end
  rescue Net::ReadTimeout, Net::OpenTimeout => e
    warn <<~MSG
      Request timed out.
      The deployment may still have been created on the server.
      Check https://central.sonatype.com/publishing/deployments for status.
    MSG
    raise e
  end

  unless res.is_a?(Net::HTTPSuccess)
    warn "Failed to trigger upload (HTTP #{res.code}): #{res.body}"
    exit(1)
  end

  puts 'Upload triggered — Sonatype will automatically validate and publish.'
end

desc 'Build Java Client Jars'
task :build do |_task, arguments|
  java_release_targets.each { |target| Bazel.execute('build', arguments.to_a, target) }
end

desc 'Build the selenium client jars'
task :client do |_task, arguments|
  Bazel.execute('build', arguments.to_a, '//java/src/org/openqa/selenium:client-combined')
end

desc 'Build Grid Server'
task :grid do |_task, arguments|
  Bazel.execute('build', arguments.to_a, '//java/src/org/openqa/selenium/grid:executable-grid')
end

desc 'Package Java bindings and grid into releasable packages and stage for release'
task :package do |_task, arguments|
  args = arguments.to_a.empty? ? ['--config=release'] : arguments.to_a
  Bazel.execute('build', args, '//java/src/org/openqa/selenium:client-zip')
  Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:server-zip')
  Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:executable-grid')

  mkdir_p 'build/dist'
  Dir.glob('build/dist/*{java,server}*').each { |file| FileUtils.rm_f(file) }

  FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/server-zip.zip',
                 "build/dist/selenium-server-#{java_version}.zip")
  FileUtils.chmod(0o644, "build/dist/selenium-server-#{java_version}.zip")
  FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/client-zip.zip',
                 "build/dist/selenium-java-#{java_version}.zip")
  FileUtils.chmod(0o644, "build/dist/selenium-java-#{java_version}.zip")
  FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/selenium',
                 "build/dist/selenium-server-#{java_version}.jar")
  FileUtils.chmod(0o755, "build/dist/selenium-server-#{java_version}.jar")
end

desc 'Validate Java release credentials'
task :check_credentials do |_task, arguments|
  nightly = arguments.to_a.include?('nightly')

  has_env = (ENV['MAVEN_USER'] || ENV.fetch('SEL_M2_USER',
                                            nil)) && (ENV['MAVEN_PASSWORD'] || ENV.fetch('SEL_M2_PASS', nil))
  settings = File.join(Dir.home, '.m2', 'settings.xml')
  has_file = File.exist?(settings) && File.read(settings).include?('<id>central</id>')
  unless has_env || has_file
    raise 'Missing Maven credentials: set MAVEN_USER/MAVEN_PASSWORD or configure ~/.m2/settings.xml'
  end

  next if nightly

  has_gpg = system('which gpg >/dev/null 2>&1') || system('where gpg >NUL 2>&1')
  raise 'Missing GPG: gpg command not found (required for signing releases)' unless has_gpg
end

desc 'Deploy all jars to Maven'
task :release do |_task, arguments|
  args = arguments.to_a
  nightly = args.delete('nightly')

  unless nightly
    already_published = begin
      Rake::Task['java:verify'].invoke
      true
    rescue StandardError
      false
    ensure
      Rake::Task['java:verify'].reenable
    end

    if already_published
      puts 'Java packages already published — skipping release.'
      next
    end
  end

  Rake::Task['java:check_credentials'].invoke(*(nightly ? ['nightly'] : []))

  ENV['MAVEN_USER'] ||= ENV.fetch('SEL_M2_USER', nil)
  ENV['MAVEN_PASSWORD'] ||= ENV.fetch('SEL_M2_PASS', nil)
  token = sonatype_auth_token

  repo_domain = 'central.sonatype.com'
  repo = if nightly
           "#{repo_domain}/repository/maven-snapshots"
         else
           "ossrh-staging-api.#{repo_domain}/service/local/staging/deploy/maven2/"
         end
  ENV['MAVEN_REPO'] = "https://#{repo}"
  ENV['GPG_SIGN'] = (!nightly).to_s

  if nightly
    puts 'Updating Java version to nightly...'
    Rake::Task['java:version'].invoke('nightly')
  end

  puts 'Packaging Java artifacts...'
  Rake::Task['java:package'].invoke('--config=release')
  Rake::Task['java:build'].invoke('--config=release')

  puts "Releasing Java artifacts to Maven repository at '#{ENV.fetch('MAVEN_REPO', nil)}'"
  java_release_targets.each { |target| Bazel.execute('run', ['--config=release'], target) }

  next if nightly

  trigger_sonatype_publish(token)
end

desc 'Verify Java packages are published on Maven Central'
task :verify do
  SeleniumRake.verify_package_published("https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-java/#{java_version}/selenium-java-#{java_version}.pom")
end

desc 'Install jars to local m2 directory'
task :install do
  java_release_targets.each do |p|
    Bazel.execute('run',
                  ['--define',
                   "maven_repo=file://#{Dir.home}/.m2/repository",
                   '--define',
                   'gpg_sign=false'],
                  p)
  end
end

desc 'Generate and stage Java documentation'
task :docs do |_task, arguments|
  if java_version.include?('SNAPSHOT') && !arguments.to_a.include?('force')
    abort('Aborting documentation update: snapshot versions should not update docs.')
  end

  Rake::Task['java:docs_generate'].invoke

  FileUtils.rm_rf('build/docs/api/java')
  FileUtils.mkdir_p('build/docs/api/java')
  out = 'bazel-bin/java/src/org/openqa/selenium/grid/all-javadocs.jar'

  cmd = %(cd build/docs/api/java && jar xf "../../../../#{out}" 2>&1)
  cmd = cmd.tr('/', '\\').tr(':', ';') if /mswin|msys|mingw32/.match?(RbConfig::CONFIG['host_os'])
  raise 'could not unpack javadocs' unless system(cmd)

  File.open('build/docs/api/java/stylesheet.css', 'a') do |file|
    file.write(<<~STYLE
      /* Custom selenium-specific styling */
      .blink {
        animation: 2s cubic-bezier(0.5, 0, 0.85, 0.85) infinite blink;
      }

      @keyframes blink {
        50% {
          opacity: 0;
        }
      }

    STYLE
              )
  end
end

desc 'Generate Java documentation without staging'
task :docs_generate do
  puts 'Generating Java documentation'
  Bazel.execute('build', [], '//java/src/org/openqa/selenium/grid:all-javadocs')
end

desc 'Update Maven dependencies'
task :update do
  puts 'Updating Maven dependencies'
  # Make sure things are in a good state to start with
  Rake::Task['java:pin'].invoke

  file_path = 'MODULE.bazel'
  content = File.read(file_path)
  output = nil
  Bazel.execute('run', [], '@maven//:outdated') do |out|
    output = out
  end

  versions = output.scan(/(\S+) \[\S+ -> (\S+)\]/).to_h
  versions.each do |artifact, version|
    if artifact.match?('graphql') && version.match?(/\A(\d{6}-|0\.0\.0-)/)
      # Maven Central indexes non-stable date-based artifacts that sort as "latest" which breaks for graphql
      # https://github.com/graphql-java/graphql-java/discussions/3187
      # Fall back to calling maven directly for this
      version = maven_stable_release(artifact)
      next if version.nil?
    end
    if artifact.start_with?('net.bytebuddy:') && version.match?(/-jdk\d/)
      # Byte Buddy publishes -jdkN compat variants alongside regular releases; Maven sorts those
      # variants as newer than the regular release. Selenium targets Java 8+ so we want the
      # regular release without the JDK suffix.
      # https://github.com/SeleniumHQ/selenium/issues/17355
      version = maven_stable_release(artifact)
      next if version.nil?
    end
    content.sub!(/#{Regexp.escape(artifact)}:([\d.-]+(?:[-.]?[A-Za-z0-9]+)*)/, "#{artifact}:#{version}")
  end
  File.write(file_path, content)

  Rake::Task['java:pin'].reenable
  Rake::Task['java:pin'].invoke
end

def maven_stable_release(artifact)
  require 'rexml/document'
  group_id, artifact_id = artifact.split(':', 2)
  group_path = group_id.tr('.', '/')
  uri = URI("https://repo1.maven.org/maven2/#{group_path}/#{artifact_id}/maven-metadata.xml")
  xml = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 10) do |http|
    http.get(uri.request_uri).body
  end
  doc = REXML::Document.new(xml)
  versions = doc.elements.to_a('metadata/versioning/versions/version').map(&:text)
  stable = versions.grep(/\A\d+\.\d+(\.\d+)*\z/)
  stable.max_by { |v| Gem::Version.new(v) }
rescue StandardError => e
  puts "WARNING — Failed to fetch stable release for #{artifact}: #{e.message}"
  nil
end

desc 'Pin Maven dependencies'
task :pin do
  args = ['--repo_env=RULES_JVM_EXTERNAL_REPIN=1']
  Bazel.execute('run', args, '@maven//:pin')
end

desc 'Update Java changelog'
task :changelogs do
  header = "v#{java_version}\n======"
  SeleniumRake.update_changelog(java_version, 'java', 'java/src/org/', 'java/CHANGELOG', header)
end

desc 'Update Java version'
task :version, [:version] do |_task, arguments|
  old_version = java_version
  new_version = SeleniumRake.updated_version(old_version, arguments[:version], '-SNAPSHOT')
  puts "Updating Java from #{old_version} to #{new_version}"

  file = 'java/version.bzl'
  text = File.read(file).gsub(old_version, new_version)
  File.open(file, 'w') { |f| f.puts text }
end

desc 'Format Java code with google-java-format'
task :format do
  puts '  Running google-java-format...'
  java_files = Dir.glob(File.join(Dir.pwd, 'java', '**', '*.java'))
  return if java_files.empty?

  Tempfile.create('google-java-format-files') do |f|
    java_files.each { |file| f.puts(file) }
    f.flush

    args = ['--', '--replace', "@#{f.path}"]
    Bazel.execute('run', args, '//scripts:google-java-format')
  end
end

# ErrorProne runs at build time, SpotBugs runs as test targets in RBE
desc 'Run Java linter (docs only, other linting happens during build/test)'
task :lint do
  Rake::Task['java:docs_generate'].invoke
end
