# -*- ruby -*-
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

require "digest/sha2"
require "json"
require "open-uri"
require "tmpdir"

require_relative "helper"

project_label = "Apache Arrow Flight SQL adapter for PostgreSQL"

version = Helper.detect_version
archive_base_name = "apache-arrow-flight-sql-postgresql-#{version}"
archive_name = "#{archive_base_name}.tar.gz"

file archive_name do
  sh("git",
     "archive",
     "HEAD",
     "--output", archive_name,
     "--prefix", "#{archive_base_name}/")
end
desc "Create #{archive_name}"
task :dist => archive_name

def build_doc(output_directory, release: nil, for_publish: false)
  env = {}
  env["RELEASE"] = release if release
  sh(env,
     "sphinx-build",
     "-b", "html",
     "-j", "auto",
     "doc/source",
     output_directory)
  if for_publish
    rm_f("#{output_directory}/.buildinfo")
    rm_rf("#{output_directory}/.doctrees")
  end
end

namespace :doc do
  desc "Build HTML documentation"
  task :html do
    build_doc("doc/build")
  end

  desc "Publish HTML documentation"
  task :publish do
    site = ENV["ASF_SITE"] || "site"
    asf_yaml = File.expand_path(".asf.yaml")
    index_html = File.expand_path("doc/index.html")

    Dir.mktmpdir do |tmp|
      is_release = (ENV["GITHUB_REF_TYPE"] == "tag")
      if is_release
        tag = ENV["GITHUB_REF_NAME"]
        new_version = tag.gsub(/-rc\d+\z/, "")
        is_release_candiate = (tag != new_version)
        new_doc = "#{tmp}/new"
        build_doc(new_doc, for_publish: true)
        unless is_release_candiate
          current_doc = "#{tmp}/current"
          build_doc(current_doc, release: "current", for_publish: true)
        end
      else
        devel_doc = "#{tmp}/devel"
        build_doc(devel_doc, release: "devel", for_publish: true)
      end

      add = lambda do |source, destination|
        rm_rf(destination)
        cp_r(source, destination)
        sh("git", "add", "--force", destination)
      end

      cd("site") do
        add.call(asf_yaml, ".asf.yaml")
        add.call(index_html, "index.html")
        if is_release
          add.call(new_doc, new_version)
          add.call(current_doc, "current") unless is_release_candiate
        else
          add.call(devel_doc, "devel")
        end
        sh("git", "commit", "-m", "Publish", "--allow-empty")
        unless ENV["GITHUB_EVENT_NAME"] == "pull_request"
          dry_run = []
          dry_run << "--dry-run" unless ENV["GITHUB_REF_NAME"] == "main"
          sh("git", "push", *dry_run, "origin", "asf-site:asf-site")
        end
      end
    end
  end
end

def load_env
  env_file = "dev/release/.env"
  unless File.exist?(env_file)
    raise "must create #{env_file} from #{env_file}.example"
  end
  File.readlines(env_file, chomp: true).each do |line|
    line.strip!
    next if line.empty?
    next if line.start_with?("#")
    name, value = line.split("=", 2)
    next if value.nil?
    name.strip!
    value.strip!
    ENV[name] ||= value
  end
end

def env_value(name, default=nil)
  value = ENV[name]
  if value.nil? and default.nil?
    raise "must set environment variable: #{name}"
  end
  value || default
end

def push_automatically?
  env_value("PUSH", "no") == "yes"
end

def github_repository
  env_value("GITHUB_REPOSITORY", "apache/arrow-flight-sql-postgresql")
end

def gpg_key_id
  env_value("GPG_KEY_ID")
end

def arrow_source
  env_value("ARROW_SOURCE")
end

def release_remote
  env_value("RELEASE_REMOTE", "release")
end

def sh_capture_output(*command_line)
  IO.pipe do |read, write|
    sh(*command_line, out: write)
    write.close
    read.read
  end
end

def detect_latest_rc(version)
  rc_tags = sh_capture_output("git", "tag").each_line(chomp: true).select do |tag|
    tag.start_with?("#{version}-rc")
  end
  rcs = rc_tags.collect do |rc_tag|
    Integer(rc_tag.delete_prefix("#{version}-rc"), 10)
  end
  rcs.max
end

def package_directories
  Dir.glob("package/postgresql-*")
end

def debian_changelog_latest_version(changelog_path)
  (File.readlines(changelog_path)[0] || "")[/\(([\d.]+)-\d+\)/, 1]
end

def git_remote_url(name)
  sh_capture_output("git", "remote", "get-url", "--push", name).chomp
end

def git_current_branch
  sh_capture_output("git", "rev-parse", "--abbrev-ref", "HEAD").chomp
end

def git_last_commit(branch="HEAD")
  sh_capture_output("git", "log", "-n", "1", "--format=format:%H", branch).chomp
end

def repository_url_extract_repository(url)
  if url.start_with?("https://")
    # https://github.com/apache/arrow-flight-sql-postgresql.git ->
    # apache/arrow-flight-sql-postgresql.git
    repository = url.split("/")[-2..-1].join("/")
  else
    # git@github.com:apache/arrow-flight-sql-postgresql.git ->
    # apache/arrow-flight-sql-postgresql.git
    repository = url.split(":").last
  end
  repository.delete_suffix(".git")
end

def repository_last_commit(repository)
  URI("https://api.github.com/repos/#{repository}/branches/main").open do |input|
    JSON.parse(input.read)["commit"]["sha"]
  end
end

def ci_conclusions(repository, branch, commit)
  jq_filter = ".[] | select(.headSha == \"#{commit}\") | .conclusion"
  sh_capture_output("gh", "run", "list",
                    "--repo", repository,
                    "--branch", branch,
                    "--json", "headSha,conclusion",
                    "--jq", jq_filter).
    lines(chomp: true)
end

def validate_rc(version)
  package_directories.each do |dir|
    latest_version = debian_changelog_latest_version("#{dir}/debian/changelog")
    unless latest_version == version
      raise "'rake release:rc:prepare && git push' is needed"
    end
  end

  release_notes = File.read("doc/source/release-notes.md").split(/^## Version /)
  latest_release_note = release_notes[1]
  latest_release_note_version = latest_release_note.lines[0].strip
  unless latest_release_note_version == version
    raise "add a release note to doc/source/release-notes.md"
  end

  current_branch = git_current_branch
  unless current_branch == "main"
    raise "work on main not a branch: #{current_branch}"
  end

  local_commit = git_last_commit
  release_repository_url = git_remote_url(release_remote)
  release_repository = repository_url_extract_repository(release_repository_url)
  remote_commit = repository_last_commit(release_repository)
  unless remote_commit == local_commit
    raise "local commit isn't synchronized with release remote: #{release_repository}"
  end

  conclusions = ci_conclusions(release_repository, "main", remote_commit)
  unless conclusions.all? {|conclusion| conclusion == "success"}
    raise "CI wasn't succeeded: https://github.com/#{release_repository}/commit/#{remote_commit}"
  end
end

def ensure_package_job_finished(rc_tag)
  run_id = nil
  while run_id.nil?
    runs = IO.pipe do |read, write|
      sh("gh", "run", "list",
         "--json", "databaseId,headBranch",
         "--repo", github_repository,
         "--workflow", "package.yaml",
         out: write)
      write.close
      read.read
    end
    run = JSON.parse(runs).find {|rc| rc["headBranch"] == rc_tag}
    run_id = run&.fetch("databaseId")
  end
  sh("gh", "run", "watch",
     "--repo", github_repository,
     "--exit-status",
     run_id.to_s)
end

def re_run_verify_rc_jobs(rc_tag)
  run_id = sh_capture_output("gh", "run", "list",
                             "--branch", rc_tag,
                             "--jq", ".[0].databaseId",
                             "--json", "databaseId",
                             "--repo", github_repository,
                             "--workflow", "verify-rc.yaml").chomp
  sh("gh", "run", "rerun",
     "--repo", github_repository,
     run_id)
end

task :env do
  load_env
end

namespace :release do
  namespace :rc do
    desc "Prepare new release"
    task :prepare => :env do
      load_env
      prepare_branch = "prepare-#{version}"
      sh("git", "switch", "-c", prepare_branch)
      package_directories.each do |dir|
        cd(dir) do
          debian_changelog = "debian/changelog"
          latest_version = debian_changelog_latest_version(debian_changelog)
          next if latest_version == version
          ruby("-S", "rake", "version:update")
          sh("git", "add", debian_changelog)
        end
      end
      sh("git", "commit", "-m", "Prepare #{version}")
      if push_automatically?
        sh("git", "push", "origin", prepare_branch)
      else
        puts("Push #{prepare_branch}:")
        puts("  git push origin #{prepare_branch}")
      end
      puts("Open a PR:")
      puts("  https://github.com/apache/arrow-flight-sql-postgresql/pulls")
    end

    desc "Ensure remote for releasing"
    task :ensure_release_remote => :env do
      begin
        git_remote_url(release_remote)
      rescue RuntimeError => error
        raise unless error.message.start_with?("Command failed")
        sh("git", "remote", "add",
           release_remote,
           "git@github.com:apache/arrow-flight-sql-postgresql.git")
      end
    end

    desc "Validation before a new RC"
    task :validate => :env do
      validate_rc(version)
    end

    desc "Tag for a new RC"
    task :tag => [:ensure_release_remote, :validate] do
      new_rc = (detect_latest_rc(version) || 0) + 1
      rc_tag = "#{version}-rc#{new_rc}"
      sh("git", "tag",
         "-a", rc_tag,
         "-m", "#{project_label} #{version} RC #{new_rc}")
      if push_automatically?
        sh("git", "push", release_remote, rc_tag)
      else
        puts("Push #{rc_tag}:")
        puts("  git push #{release_remote} #{rc_tag}")
      end
    end

    desc "Sign the latest RC"
    task :sign => :env do
      rc = detect_latest_rc(version)
      if rc.nil?
        raise "'rake release:rc:tag && git push ...' is needed"
      end
      rc_tag = "#{version}-rc#{rc}"
      ensure_package_job_finished(rc_tag)
      Dir.mktmpdir do |tmp|
        sh("gh", "release", "download",
           "--dir", tmp,
           "--pattern", archive_name,
           "--repo", github_repository,
           rc_tag)
        tmp_archive_name = "#{tmp}/#{archive_name}"
        tmp_sign_name = "#{tmp_archive_name}.asc"
        sh("gpg",
           "--armor",
           "--detach-sign",
           "--local-user", gpg_key_id,
           "--output", tmp_sign_name,
           "#{tmp_archive_name}")
        tmp_checksum_name = "#{tmp_archive_name}.sha512"
        File.open(tmp_checksum_name, "w") do |output|
          checksum = Digest::SHA512.file(tmp_archive_name)
          output.puts("#{checksum}  #{archive_name}")
        end
        sh("gh", "release", "upload",
           "--clobber",
           "--repo", github_repository,
           rc_tag,
           tmp_sign_name,
           tmp_checksum_name)
      end
    end

    desc "Upload Linux packages"
    task :linux => :env do
      rc = detect_latest_rc(version)
      if rc.nil?
        raise "'rake release:rc:tag && git push ...' is needed"
      end
      rc_tag = "#{version}-rc#{rc}"
      Dir.mktmpdir do |tmp|
        sh("gh", "release", "download",
           "--dir", tmp,
           "--pattern", "debian-*.tar.gz",
           "--pattern", "ubuntu-*.tar.gz",
           "--repo", github_repository,
           rc_tag)
        Dir.glob("#{tmp}/*.tar.gz") do |tar_gz|
          sh("tar", "xf", tar_gz,
             "--directory", tmp,
             "--one-top-level")
        end
        env = {
          "ARROW_ARTIFACTS_DIR" => tmp,
          "DEB_PACKAGE_NAME" => "apache-arrow-flight-sql-postgresql",
          "UPLOAD_DEFAULT" => "0",
          "UPLOAD_DEBIAN" => "1",
          "UPLOAD_UBUNTU" => "1",
        }
        sh(env,
           "#{arrow_source}/dev/release/05-binary-upload.sh",
           version,
           rc.to_s)
      end
    end

    desc "Re-run verify RC CI jobs"
    task :verify => :env do
      rc = detect_latest_rc(version)
      re_run_verify_rc_jobs("#{version}-rc#{rc}")
    end

    desc "Generate a release vote e-mail"
    task :vote => :env do
      rc = detect_latest_rc(version)
      if rc.nil?
        raise "'rake release:rc:tag && git push ...' is needed"
      end
      rc_tag = "#{version}-rc#{rc}"
      commit = git_last_commit(rc_tag)
      puts(<<-MAIL)
To: dev@arrow.apache.org
Subject: [VOTE] Release Apache Arrow Flight SQL adapter for PostgreSQL ${version} - RC#{rc}

Hi,

I would like to propose the following release candidate (RC#{rc}) of
Apache Arrow Flight SQL adapter for PostgreSQL version #{version}.

This release candidate is based on commit: #{commit} [1]

The source release rc#{rc} and changelog is hosted at [2].
The binary artifacts are hosted at [3][4].

Please download, verify checksums and signatures, build and run,
and vote on the release. See [5] for how to validate a release
candidate.

The vote will be open for at least 24 hours because Apache Arrow
Flight SQL adapter for PostgreSQL doesn't reach 1.0.0 yet.

[ ] +1 Release this as Apache Arrow Flight SQL adapter for PostgreSQL #{version}
[ ] +0
[ ] -1 Do not release this as Apache Arrow Flight SQL adapter for PostgreSQL #{version}
       because...

[1]: https://github.com/apache/arrow-flight-sql-postgresql/commit/#{commit}
[2]: https://github.com/apache/arrow-flight-sql-postgresql/releases/tag/#{rc_tag}
[3]: https://apache.jfrog.io/artifactory/arrow/debian-rc/
[4]: https://apache.jfrog.io/artifactory/arrow/ubuntu-rc/
[5]: https://arrow.apache.org/flight-sql-postgresql/devel/development/release.html#how-to-verify
      MAIL
    end

    namespace :publish do
      desc "Publish to https://dist.apache.org/"
      task :apache => :env do
        rc = detect_latest_rc(version)
        rc_tag = "#{version}-rc#{rc}"
        Dir.mktmpdir do |tmp|
          sh("svn", "co",
             "--depth=empty",
             "https://dist.apache.org/repos/dist/release/arrow",
             tmp)
          release_dir = "#{tmp}/apache-arrow-flight-sql-postgresql-#{verison}"
          mkdir_p(release_dir)
          sh("gh", "release", "download",
             "--dir", release_dir,
             "--pattern", "#{archive_name}*",
             "--repo", github_repository,
             rc_tag)
          sh("svn", "add", release_dir)
          sh("svn", "ci", "-m", "#{project_label} #{version}", release_dir)
        end
      end

      desc "Register a new release to https://reporter.apache.org/"
      task :reporter do
        sh("open", "https://reporter.apache.org/addrelease.html?arrow")
      end

      desc "Publish Linux packages"
      task :linux => :env do
        rc = detect_latest_rc(version)
        env = {
          "UPLOAD_DEFAULT" => "0",
          "UPLOAD_DEBIAN" => "1",
          "UPLOAD_UBUNTU" => "1",
        }
        sh(env,
           "#{arrow_source}/dev/release/post-02-binary.sh",
           version,
           rc.to_s)
      end

      desc "Tag #{version}"
      task :tag => :env do
        rc = detect_latest_rc(version)
        rc_tag = "#{version}-rc#{rc}"
        sh("git", "tag",
           "-a", version,
           "-m", "#{project_label} #{version}",
           "#{rc_tag}^{}")
        if push_automatically?
          sh("git", "push", release_remote, version)
        else
          puts("Push #{version}:")
          puts("  git push #{release_remote} #{version}")
        end
      end
    end

    desc "Publish the latest RC as a new release"
    task :publish => [
           "release:rc:publish:apache",
           "release:rc:publish:reporter",
           "release:rc:publish:linux",
           "release:rc:publish:tag",
         ]
  end

  namespace :announce do
    desc "Show blog announce template"
    task :blog => :env do
      previous_version = env_value("PREVIOUS_VERSION")
      commit_range = "#{previous_version}..#{version}"
      n_commits = sh_capture_output("git", "rev-list", "--count", commit_range).chomp
      contributor_command = [
        "git",
        "shortlog",
        "--perl-regexp",
        "--author='^((?!dependabot\[bot\]).*)$'",
        "-sn",
        commit_range,
      ]
      contributors = sh_capture_output(*contributor_command)
      n_contributors = contributors.lines.size
      post_date = env_value("POST_DATE", Date.today.strftime("%Y-%m-%d"))
      puts(<<-POST)
---
layout: post
title: "Apache Arrow Flight SQL adapter for PostgreSQL #{version} Release"
date: "#{post_date} 00:00:00"
author: pmc
categories: [release]
---
{% comment %}
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to you under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License.  You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
{% endcomment %}

The Apache Arrow team is pleased to announce the #{version} release of
the Apache Arrow Flight SQL adapter for PostgreSQL. This includes
[**#{n_commits} commits][commits] from [**#{n_contributors} distinct
contributors**][contributors].

The release notes below are not exhaustive and only expose selected
highlights of the release. Many other bugfixes and improvements have
been made: we refer you to [the complete release notes][release-note].

## Release Highlights

<!-- TODO: fill this portion in. -->

## Contributors

```console
$ #{contributor_command.join(" ")}
#{contributors}
```

## Roadmap

<!-- TODO: fill this portion in. -->

## Getting Involved

We welcome questions and contributions from all interested. Issues can
be filed on [GitHub][issues], and questions can be directed to GitHub or
[the Arrow mailing lists][mailing-list].

[commits]: ${MILESTONE_URL}
[contributors]: #contributors
[release-note]: https://arrow.apache.org/flight-sql-postgresql/current/release-notes.html\#version-#{version}
[issues]: https://github.com/apache/arrow-flight-sql-postgresql/issues
[mailing-list]: {% link community.md %}
      POST
    end

    desc "Show mail announce template"
    task :mail => :env do
      blog_path_date = Date.today("%Y/%m/%d")
      # Extract the first "## ..." section.
      overview = File.read("doc/source/overview.md").split(/^## /)[1]
      # Remove links:
      #   [Apache Arrow Flight SQL][apache-arrow-flight-sql] ->
      #   Apache Arrow Flight SQL
      overview.gsub!(/\[(.+?)\]\[.+?\]/m, "\\1")
      puts(<<-MAIL)
To: announce@apache.org, user@arrow.apache.org, dev@arrow.apache.org
From: XXX@apache.org
Subject: [ANNOUNCE] Apache Arrow Flight SQL adapter for PostgreSQL #{version} released

The Apache Arrow team is pleased to announce the #{version} release of
the Apache Arrow Flight SQL adapter for PostgreSQL.

The release is available now from our website:
  https://arrow.apache.org/flight-sql-postgresql/#{version}/install/

Read about what's new in the release:
  https://arrow.apache.org/blog/#{blog_path_date}/flight-sql-postgresql-#{version}-release/

Release note:
  https://arrow.apache.org/flight-sql-postgresql/#{version}/release-note.html\#version-#{version}


#{overview}


Please report any feedback to the GitHub issues or mailing lists:
  * GitHub: https://github.com/apache/arrow-flight-sql-postgresql/issues
  * ML: https://arrow.apache.org/community/


Thanks,
-- 
The Apache Arrow community
      MAIL
    end

    desc "Show PostgreSQL announce template"
    task :postgresql => :env do
      puts(<<-ANNOUNCE)
TODO
      ANNOUNCE
    end
  end

  namespace :version do
    desc "Bump version"
    task :bump => :env do
      new_version = env_value("NEW_VERSION")
      bump_version_branch = "bump-version-#{new_version}"
      sh("git", "switch", "-c", bump_version_branch)
      meson_build = File.read("meson.build")
      new_meson_build = meson_build.gsub(/version: '.+?'/) do
        "version: '#{new_version}'"
      end
      File.write("meson.build", new_meson_build)
      sh("git", "add", "meson.build")
      sh("git", "commit", "-m", "Bump version to #{new_version}")
      if push_automatically?
        sh("git", "push", "origin", bump_version_branch)
      else
        puts("Push #{bump_version_branch}:")
        puts("  git push origin #{bump_version_branch}")
      end
      puts("Open a PR:")
      puts("  https://github.com/apache/arrow-flight-sql-postgresql/pulls")
    end
  end
end
