class Dependabot::Uv::FileUpdater::LockFileUpdater

def create_or_update_lock_file?

def create_or_update_lock_file?
  T.must(dependency).requirements.select { _1[:file].end_with?(*REQUIRED_FILES) }.any?
end

def declaration_regex(dep, old_req)

def declaration_regex(dep, old_req)
  escaped_name = Regexp.escape(dep.name)
  # Extract the requirement operator and version
  operator = old_req.fetch(:requirement).match(/^(.+?)[0-9]/)&.captures&.first
  # Escape special regex characters in the operator
  escaped_operator = Regexp.escape(operator) if operator
  # Match various formats of dependency declarations:
  # 1. "dependency==1.0.0" (with quotes around the entire string)
  # 2. dependency==1.0.0 (without quotes)
  # The declaration should only include the package name, operator, and version
  # without the enclosing quotes
  /
    ["']?(?<declaration>#{escaped_name}\s*#{escaped_operator}[\d\.\*]+)["']?
  /x
end

def dependency

def dependency
  # For now, we'll only ever be updating a single dependency
  T.must(dependencies.first)
end

def escape(name)

def escape(name)
  Regexp.escape(name).gsub("\\-", "[-_.]")
end

def fetch_updated_dependency_files

def fetch_updated_dependency_files
  return [] unless create_or_update_lock_file?
  updated_files = []
  if file_changed?(pyproject)
    updated_files <<
      updated_file(
        file: T.must(pyproject),
        content: T.must(updated_pyproject_content)
      )
  end
  if lockfile
    # Use updated_lockfile_content which might raise if the lockfile doesn't change
    new_content = updated_lockfile_content
    raise "Expected lockfile to change!" if T.must(lockfile).content == new_content
    updated_files << updated_file(file: T.must(lockfile), content: new_content)
  end
  updated_files
end

def file_changed?(file)

def file_changed?(file)
  return false unless file
  dependencies.any? do |dep|
    dep.requirements.any? { |r| r[:file] == file.name } &&
      requirement_changed?(file, dep)
  end
end

def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)

def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
  @dependencies = dependencies
  @dependency_files = dependency_files
  @credentials = credentials
  @index_urls = index_urls
  @prepared_pyproject = T.let(nil, T.nilable(String))
  @updated_lockfile_content = T.let(nil, T.nilable(String))
  @pyproject = T.let(nil, T.nilable(Dependabot::DependencyFile))
end

def language_version_manager

def language_version_manager
  @language_version_manager ||= T.let(
    LanguageVersionManager.new(
      python_requirement_parser: python_requirement_parser
    ), T.nilable(LanguageVersionManager)
  )
end

def lock_index_options

def lock_index_options
  credentials
    .select { |cred| cred["type"] == "python_index" }
    .map do |cred|
    authed_url = AuthedUrlBuilder.authed_url(credential: cred)
    if cred.replaces_base?
      "--default-index #{authed_url}"
    else
      "--index #{authed_url}"
    end
  end
end

def lock_options

def lock_options
  options = lock_index_options
  options.join(" ")
end

def lock_options_fingerprint(options)

def lock_options_fingerprint(options)
  options.sub(
    /--default-index\s+\S+/, "--default-index <default_index>"
  ).sub(
    /--index\s+\S+/, "--index <index>"
  )
end

def lockfile

def lockfile
  @lockfile ||= T.let(uv_lock, T.nilable(Dependabot::DependencyFile))
end

def normalise(name)

def normalise(name)
  NameNormaliser.normalise(name)
end

def normalize_line_endings(content, reference)

def normalize_line_endings(content, reference)
  # Check if reference has escaped newlines like "\n" +
  if reference.include?("\\n")
    content.gsub("\n", "\\n")
  else
    content
  end
end

def prepared_pyproject

def prepared_pyproject
  @prepared_pyproject ||=
    begin
      content = updated_pyproject_content
      content = sanitize(T.must(content))
      content
    end
end

def pyproject

def pyproject
  @pyproject ||= T.let(dependency_files.find { |f| f.name == "pyproject.toml" },
                       T.nilable(Dependabot::DependencyFile))
end

def python_helper_path

def python_helper_path
  NativeHelpers.python_helper_path
end

def python_requirement_parser

def python_requirement_parser
  @python_requirement_parser ||= T.let(
    FileParser::PythonRequirementParser.new(
      dependency_files: dependency_files
    ), T.nilable(FileParser::PythonRequirementParser)
  )
end

def replace_dep(dep, content, new_r, old_r)

def replace_dep(dep, content, new_r, old_r)
  new_req = new_r[:requirement]
  old_req = old_r[:requirement]
  declaration_regex = declaration_regex(dep, old_r)
  declaration_match = content.match(declaration_regex)
  if declaration_match
    declaration = declaration_match[:declaration]
    new_declaration = T.must(declaration).sub(old_req, new_req)
    content.sub(T.must(declaration), new_declaration)
  else
    content
  end
end

def requirement_changed?(file, dependency)

def requirement_changed?(file, dependency)
  changed_requirements =
    dependency.requirements - T.must(dependency.previous_requirements)
  changed_requirements.any? { |f| f[:file] == T.must(file).name }
end

def run_command(command, fingerprint: nil)

def run_command(command, fingerprint: nil)
  Dependabot.logger.info("Running command: #{command}")
  SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
end

def run_update_command

def run_update_command
  options = lock_options
  options_fingerprint = lock_options_fingerprint(options)
  # Use pyenv exec to ensure we're using the correct Python environment
  command = "pyenv exec uv lock --upgrade-package #{T.must(dependency).name} #{options}"
  fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
  run_command(command, fingerprint:)
end

def sanitize(pyproject_content)

def sanitize(pyproject_content)
  PyprojectPreparer
    .new(pyproject_content: pyproject_content)
    .sanitize
end

def sanitize_env_name(url)

def sanitize_env_name(url)
  url.gsub(%r{^https?://}, "").gsub(/[^a-zA-Z0-9]/, "_").upcase
end

def setup_python_environment

def setup_python_environment
  # Use LanguageVersionManager to determine and install the appropriate Python version
  Dependabot.logger.info("Setting up Python environment using LanguageVersionManager")
  begin
    # Install the required Python version
    language_version_manager.install_required_python
    # Set the local Python version
    python_version = language_version_manager.python_version
    Dependabot.logger.info("Setting Python version to #{python_version}")
    SharedHelpers.run_shell_command("pyenv local #{python_version}")
    # We don't need to install uv as it should be available in the Docker environment
    Dependabot.logger.info("Using pre-installed uv package")
  rescue StandardError => e
    Dependabot.logger.warn("Error setting up Python environment: #{e.message}")
    Dependabot.logger.info("Falling back to system Python")
  end
end

def updated_dependency_files

def updated_dependency_files
  @updated_dependency_files ||= T.let(fetch_updated_dependency_files,
                                      T.nilable(T::Array[Dependabot::DependencyFile]))
end

def updated_file(file:, content:)

def updated_file(file:, content:)
  updated_file = file.dup
  updated_file.content = content
  updated_file
end

def updated_lockfile_content

def updated_lockfile_content
  @updated_lockfile_content ||=
    begin
      original_content = T.must(lockfile).content
      # Extract the original requires-python value to preserve it
      original_requires_python = T.must(original_content)
                                  .match(/requires-python\s*=\s*["']([^"']+)["']/)&.captures&.first
      # Store the original Python version requirement for later use
      @original_python_version = T.let(original_requires_python, T.nilable(String))
      new_lockfile = updated_lockfile_content_for(prepared_pyproject)
      # Normalize line endings to ensure proper comparison
      new_lockfile = normalize_line_endings(new_lockfile, T.must(original_content))
      result = new_lockfile
      # Restore the original requires-python if it exists
      if original_requires_python
        result = result.gsub(/requires-python\s*=\s*["'][^"']+["']/,
                             "requires-python = \"#{original_requires_python}\"")
      end
      result
    end
end

def updated_lockfile_content_for(pyproject_content)

def updated_lockfile_content_for(pyproject_content)
  SharedHelpers.in_a_temporary_directory do
    SharedHelpers.with_git_configured(credentials: credentials) do
      write_temporary_dependency_files(pyproject_content)
      # Set up Python environment using LanguageVersionManager
      setup_python_environment
      run_update_command
      File.read("uv.lock")
    end
  end
end

def updated_pyproject_content

def updated_pyproject_content
  content = T.must(pyproject).content
  return content unless file_changed?(T.must(pyproject))
  updated_content = content.dup
  T.must(dependency).requirements.zip(T.must(T.must(dependency).previous_requirements)).each do |new_r, old_r|
    next unless new_r[:file] == T.must(pyproject).name && T.must(old_r)[:file] == T.must(pyproject).name
    updated_content = replace_dep(T.must(dependency), T.must(updated_content), new_r, T.must(old_r))
  end
  raise DependencyFileContentNotChanged, "Content did not change!" if content == updated_content
  updated_content
end

def uv_lock

def uv_lock
  dependency_files.find { |f| f.name == "uv.lock" }
end

def write_temporary_dependency_files(pyproject_content)

def write_temporary_dependency_files(pyproject_content)
  dependency_files.each do |file|
    path = file.name
    FileUtils.mkdir_p(Pathname.new(path).dirname)
    File.write(path, file.content)
  end
  # Overwrite the pyproject with updated content
  File.write("pyproject.toml", pyproject_content)
end