require "date"
require "shopify_cli/sed"
require "octokit"

module ShopifyCLI
  class Changelog
    CHANGELOG_FILE = File.join(ShopifyCLI::ROOT, "CHANGELOG.md")
    CHANGE_CATEGORIES = %w(Added Changed Deprecated Removed Fixed Security)

    def initialize
      load(File.read(CHANGELOG_FILE))
    end

    def update_version!(new_version)
      changes[new_version] = changes["Unreleased"]
      changes[new_version][:date] = Date.today.iso8601
      changes["Unreleased"] = { changes: [], date: nil }
      save!
    end

    def update!
      pr = pr_for_current_branch
      category = CLI::UI::Prompt.ask("What type of change?", options: CHANGE_CATEGORIES)
      add_change(category, { pr_id: pr.number, desc: pr.title })
      save!
    end

    def release_notes(version)
      changes[version][:changes].map do |change_category, changes|
        <<~CHANGES
          ### #{change_category}
          #{changes.map { |change| entry(**change) }.join("\n")}
        CHANGES
      end.join("\n")
    end

    def add_change(category, change)
      changes["Unreleased"][:changes][category] << change
    end

    def entry(pr_id:, desc:)
      "* [##{pr_id}](https://github.com/Shopify/shopify-cli/pull/#{pr_id}): #{desc}"
    end

    def full_contents
      sorted_changes = changes.each_key.sort_by do |change|
        if change == "Unreleased"
          [Float::INFINITY] * 3 # end of the list
        else
          major, minor, patch = change.split(".").map(&:to_i)
          [major, minor, patch]
        end
      end.reverse
      [
        heading,
        *sorted_changes.each.map { |version| release_notes_with_header(version) }.join,
        remainder,
      ].map { |section| section.chomp << "\n" }.join
    end

    def save!
      File.write(CHANGELOG_FILE, full_contents)
    end

    private

    attr_reader :heading, :remainder

    def release_notes_with_header(version)
      header_line =
        if version == "Unreleased"
          "[Unreleased]"
        else
          date = changes[version][:date]
          "Version #{version}#{" - #{date}" if date}"
        end

      [
        "## #{header_line}",
        release_notes(version),
      ].reject(&:empty?).map { |section| section.chomp << "\n\n" }.join
    end

    def changes
      @changes ||= Hash.new do |h, k|
        h[k] = {
          date: nil,
          changes: Hash.new do |h2, k2|
            h2[k2] = []
          end,
        }
      end
    end

    def load(log)
      state = :initial
      change_category = nil
      current_version = nil
      @heading = ""
      @remainder = ""
      log.each_line do |line|
        case state
        when :initial
          if line.chomp == "\#\# [Unreleased]"
            state = :unreleased
            current_version = "Unreleased"
            # Ensure Unreleased changeset exists even if no changes have happened yet
            changes["Unreleased"]
          else
            @heading << line
          end
        when :unreleased, :prior_versions
          if /\A\#\#\# (?<category>\w+)/ =~ line
            change_category = category
          elsif %r{\A\* \[\#(?<id>\d+)\]\(https://github.com/Shopify/shopify-cli/pull/\k<id>\): (?<desc>.+)\n} =~ line
            changes[current_version][:changes][change_category] << { pr_id: id, desc: desc }
          elsif /\A\#\# Version (?<version>\d+\.\d+\.\d+)( - (?<date>\d{4}-\d{2}-\d{2}))?/ =~ line
            current_version = version
            state = :prior_versions
            major, minor, _patch = current_version.split(".")
            if major.to_i <= 2 && minor.to_i < 7
              # Changelog starts to become irregular in 2.6.x
              state = :finished
            end
            changes[current_version][:date] = date unless state == :finished
          elsif !line.match?(/\s*\n/)
            raise "Unrecognized line: #{line.inspect}"
          end
        end
        @remainder << line if state == :finished
      end
    end

    def pr_for_current_branch
      current_branch = %x(git branch --show-current).chomp
      search_term = "repo:Shopify/shopify-cli is:pr is:open head:#{current_branch}"
      results = Octokit::Client.new.search_issues(search_term)
      case results.total_count
      when 0
        raise "PR not opened yet!"
      when (2..)
        raise "Multiple open PRs, not sure which one to use for changelog!"
      end

      results.items.first
    end
  end
end