#!/usr/bin/env ruby

require 'ollama'
include Ollama
require 'tins'

def x(cmd)
  output=`#{cmd}`
  $?.success? or fail "failed to execute #{cmd.inspect}"
  output
end

def find_highest_version_tag(filename)
  File.open(filename, ?r) do |input|
    tags = []
    input.each do |line|
      line.scan(/^## \d{4}-\d{2}-\d{2} v(\d+\.\d+\.\d+)$/) do
        tags << $1
      end
    end
    tags.map {
      Tins::StringVersion::Version.new(_1)
    }.max
  end
end

def compute_change(range_from, range_to)
  range_from = range_from.to_s.sub(/\Av?/, ?v)
  range_to = range_to.to_s.sub(/\Av?/, ?v)
  range = "#{range_from}..#{range_to}"

  log=x("git log #{range}")
  $?.success? or exit 1

  date=x("git log -n1 --pretty='format:%cd' --date=short #{range_to}")

  if log.strip.empty?
    return <<~EOT

    ## #{date} #{range_to}
    EOT
  end

  base_url = ENV['OLLAMA_URL'] || 'http://%s' % ENV.fetch('OLLAMA_HOST')
  model    = ENV.fetch('OLLAMA_MODEL', 'llama3.1')

  system = <<~EOT
    You are a Ruby programmer generating a change log entry in markdown syntax,
    summarizing the code changes for a new version in a professional way.
  EOT

  prompt = <<~EOT
    - Summarize the changes in the following git log messages as bullet points.
    - List significant changes as bullet points using markdown when applicable.
    - Don't refer to single commits by sha1 hash.
    - Don't add information about changes you are not sure about.
    - Don't output any additional chatty remarks, notes, introductions,
      communications.

    #{log}
  EOT

  if ENV['DEBUG'].to_i == 1
    STDERR.puts "system:\n#{system}"
    STDERR.puts "prompt:\n#{prompt}"
  end

  options = Ollama::Options.new(
    num_ctx: 8192,
    num_predict: 1024,
    temperature: 0,
    #seed: 1337,
    top_p: 1,
    min_p: 0.1,
  )

  ollama = Client.new(base_url:, read_timeout: 120)
  changes = ollama.generate(model:, system:, prompt:, options:, stream: false).response

  return <<~EOT

    ## #{date} #{range_to}

    #{changes}
  EOT
end

x("git fetch --tags")

case command = ARGV.shift
when 'range'
  range = ARGV.shift
  if range =~ /\A(.+)\.\.(.+)\z/
    range_from, range_to = $1, $2
    puts compute_change(range_from, range_to)
  else
    fail "need range of the form v1.2.3..v1.2.4"
  end
when 'full', 'add'
  ary = []
  tags=x("git tag").lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map {
    Tins::StringVersion::Version.new(_1.sub(/\Av/, ''))
  }.sort
  if command == 'full'
    date=x("git log -n1 --pretty='format:%cd' --date=short v#{tags.first}").chomp
    ary << <<~EOT

      ## #{date} v#{tags.first}

        * Start
    EOT
    tags.each_cons(2) do |range_from, range_to|
      ary << compute_change(range_from, range_to)
    end
    ary.reverse!
    ary.unshift <<~EOT
      # Changes
    EOT
    puts ary
  else
    filename = ARGV.shift or fail 'need file to add to'
    start_tag = find_highest_version_tag(filename)
    tags = tags.drop_while { |t| t < start_tag }
    ary = []
    tags.each_cons(2) do |range_from, range_to|
      ary << compute_change(range_from, range_to)
    end
    ary.empty? and exit
    ary.reverse!
    File.open(filename) do |input|
      File.secure_write(filename) do |output|
        start_add = nil
        input.each do |line|
          if start_add.nil? && line =~ /^# Changes$/
            start_add = true
            output.puts line
            next
          end
          if start_add && line =~ /^$/
            ary.each do |change|
              STDERR.puts change
              output.puts change
            end
            output.puts line
            start_add = false
            next
          end
          output.puts line
        end
      end
    end
  end
else
  puts <<~end
    Usage: #{File.basename($0)} help|range|full|add
  end
end