# typed: strict
# frozen_string_literal: true

require "utils/git"
require "utils/popen"

# Given a {Pathname}, provides methods for querying Git repository information.
# @see Utils::Git
class GitRepository
  sig { returns(Pathname) }
  attr_reader :pathname

  sig { params(pathname: Pathname).void }
  def initialize(pathname)
    @pathname = pathname
  end

  sig { returns(T::Boolean) }
  def git_repository?
    pathname.join(".git").exist?
  end

  # Gets the URL of the Git origin remote.
  sig { returns(T.nilable(String)) }
  def origin_url
    origin_url_from_config || popen_git("config", "--local", "--get", "remote.origin.url", no_global_config: true)
  end

  # Gets the full commit hash of the HEAD commit.
  sig { params(safe: T::Boolean).returns(T.nilable(String)) }
  def head_ref(safe: false)
    popen_git("rev-parse", "--verify", "--quiet", "HEAD", safe:)
  end

  # Gets a short commit hash of the HEAD commit.
  sig { params(length: T.nilable(Integer), safe: T::Boolean).returns(T.nilable(String)) }
  def short_head_ref(length: nil, safe: false)
    short_arg = length.present? ? "--short=#{length}" : "--short"
    popen_git("rev-parse", short_arg, "--verify", "--quiet", "HEAD", safe:)
  end

  # Gets the relative date of the last commit, e.g. "1 hour ago"
  sig { returns(T.nilable(String)) }
  def last_committed
    popen_git("show", "-s", "--format=%cr", "HEAD")
  end

  # Gets the full commit hash of the HEAD commit, the relative date of the
  # last commit and the currently checked-out branch (or HEAD if the
  # repository is in a detached HEAD state) in a single Git invocation.
  sig { returns([T.nilable(String), T.nilable(String), T.nilable(String)]) }
  def head_info
    output = popen_git("show", "-s", "--format=%H%n%cr%n%D", "HEAD")
    return [nil, nil, nil] if output.nil?

    head, last_committed, refs = output.lines(chomp: true)
    branch = refs&.[](/\AHEAD -> ([^,\n]+)/, 1) || "HEAD"
    [head, last_committed, branch]
  end

  # Gets the name of the currently checked-out branch, or HEAD if the repository is in a detached HEAD state.
  sig { params(safe: T::Boolean).returns(T.nilable(String)) }
  def branch_name(safe: false)
    ref = popen_git("rev-parse", "--symbolic-full-name", "HEAD", safe:)
    return if ref.blank?
    return "HEAD" if ref == "HEAD"

    refs_format = "refs/heads/"
    return ref.delete_prefix(refs_format) if ref.start_with?(refs_format)

    raise "Unexpected HEAD ref format: #{ref}"
  end

  # Change the name of a local branch
  sig { params(old: String, new: String).void }
  def rename_branch(old:, new:)
    popen_git("branch", "-m", old, new)
  end

  # Set an upstream branch for a local branch to track
  sig { params(local: String, origin: String).void }
  def set_upstream_branch(local:, origin:)
    popen_git("branch", "-u", "origin/#{origin}", local)
  end

  # Gets the name of the default origin HEAD branch.
  sig { returns(T.nilable(String)) }
  def origin_branch_name
    ref = popen_git("symbolic-ref", "-q", "refs/remotes/origin/HEAD")
    return if ref.blank?

    refs_format = "refs/remotes/origin/"
    return ref.delete_prefix(refs_format) if ref.start_with?(refs_format)

    raise "Unexpected origin/HEAD ref format: #{ref}"
  end

  # Returns true if the repository's current branch matches the default origin branch.
  sig { returns(T.nilable(T::Boolean)) }
  def default_origin_branch?
    origin_branch_name == branch_name
  end

  # Returns the date of the last commit, in YYYY-MM-DD format.
  sig { returns(T.nilable(String)) }
  def last_commit_date
    popen_git("show", "-s", "--format=%cd", "--date=short", "HEAD")
  end

  # Returns true if the given branch exists on origin
  sig { params(branch: String).returns(T::Boolean) }
  def origin_has_branch?(branch)
    popen_git("ls-remote", "--heads", "origin", branch).present?
  end

  sig { void }
  def set_head_origin_auto
    popen_git("remote", "set-head", "origin", "--auto")
  end

  # Gets the full commit message of the specified commit, or of the HEAD commit if unspecified.
  sig { params(commit: String, safe: T::Boolean).returns(T.nilable(String)) }
  def commit_message(commit = "HEAD", safe: false)
    popen_git("log", "-1", "--pretty=%B", commit, "--", safe:, err: :out)&.strip
  end

  sig { returns(String) }
  def to_s = pathname.to_s

  private

  # Reads `remote.origin.url` straight from `.git/config` to skip spawning
  # Git on a hot path. Returns `nil` (so the caller falls back to Git) for
  # anything the canonical form does not cover: a worktree/submodule `.git`
  # file, an `include`/`includeIf` directive that can define the URL
  # elsewhere, a non-canonical section header, more than one `url` value, or a
  # value whose comments/quoting/escapes need Git's own config parser.
  sig { returns(T.nilable(String)) }
  def origin_url_from_config
    config_file = pathname/".git/config"
    return unless config_file.file?

    content = config_file.read
    return if content.match?(/^\s*\[include(?:If)?[\s"\]]/i)

    urls = content.lines
                  .slice_before { |line| line.lstrip.start_with?("[") }
                  .select { |section| section.fetch(0).strip == '[remote "origin"]' }
                  .flat_map do |section|
      section.drop(1).filter_map do |line|
        stripped = line.strip
        next if stripped.empty? || stripped.start_with?("#", ";")

        key, separator, value = stripped.partition("=")
        next if separator.empty? || !key.strip.casecmp?("url")

        value.strip
      end
    end
    return if urls.length != 1

    url = urls.fetch(0)
    return if url.empty? || url.match?(/["#;\\]/)

    url
  rescue SystemCallError
    nil
  end

  sig {
    params(args: T.untyped, safe: T::Boolean, err: T.nilable(Symbol), no_global_config: T::Boolean)
      .returns(T.nilable(String))
  }
  def popen_git(*args, safe: false, err: nil, no_global_config: false)
    unless git_repository?
      return unless safe

      raise "Not a Git repository: #{pathname}"
    end

    unless Utils::Git.available?
      return unless safe

      raise "Git is unavailable"
    end

    command = [Utils::Git.git, *args]
    command.unshift(Utils::Git.no_global_config_env) if no_global_config
    Utils.popen_read(*command, safe:, chdir: pathname, err:).chomp.presence
  end
end
