require "file_utils"
require "http/client"

BIN  = Path.home / ".local" / "bin"
SRC  = ".mol/src"
DB   = ".mol/repo"
REPO = ENV.fetch("MOL_REPO", "https://git.sr.ht/~chersbobers/mol-pkgrepo/blob/master/PACKAGES?raw=true")
OS   = {% if flag?(:win32) %}"windows"{% elsif flag?(:linux) %}"linux"{% else %}"macos"{% end %}

def run(cmd, args, dir = nil)
  Process.run(cmd, args, chdir: dir,
    output: Process::Redirect::Inherit,
    error: Process::Redirect::Inherit).success?
end

def lookup(t) : {String, String?}
  return {t, nil} if t.includes?("://")
  if File.exists?(DB)
    File.each_line(DB) do |l|
      l = l.strip; next if l.empty? || l.starts_with?('#')
      p = l.split; next unless p[0] == t
      return {p[1], p.find(&.starts_with?("B=")).try(&.[2..])}
    end
  end
  abort "#{t} not found, run: mol sync"
end

def installed?(name)
  Dir.exists?(File.join(SRC, name)) || File.exists?((BIN / name).to_s)
end

def link_bins(path)
  Dir.mkdir_p(BIN)
  [File.join(path,"bin"), File.join(path,"target","release"), path].each do |d|
    next unless Dir.exists?(d)
    Dir.each_child(d) do |f|
      full = File.join(d, f)
      next unless File.executable?(full) && !File.directory?(full) && !f.includes?('.')
      dest = (BIN / f).to_s
      FileUtils.cp(full, dest); File.chmod(dest, 0o755)
      puts "-> #{dest}"
    end
  end
end

def parse_molpkg(path) : {Array(String), Array(String)}
  file = File.join(path, "mol.pkg")
  deps  = [] of String
  build = [] of String
  return deps, build unless File.exists?(file)

  in_build   = false
  in_os      = false
  in_install = false

  File.each_line(file) do |l|
    l = l.strip
    if l.includes?("(deps")
      l.scan(/"([^"]+)"/).each { |m| deps << m[1] }
    elsif l.includes?("(build")
      in_build = true
    elsif l.includes?("(install")
      in_build = false; in_install = true
    elsif in_build && l.includes?("(#{OS}")
      in_os = true
    elsif in_build && in_os && l.starts_with?("(run")
      parts = l.scan(/"([^"]+)"/).map(&.[1])
      build << parts.join(" ") if parts.any?
    elsif l == ")" && in_os
      in_os = false
    elsif l == ")" && in_build
      in_build = false
    elsif in_install && l.includes?("(bin")
      name = l.scan(/"([^"]+)"/).first?.try(&.[1])
      if name
        src = File.join(path, name)
        src = File.join(path, "#{name}.exe") if OS == "windows" && !File.exists?(src)
        if File.exists?(src)
          dest = (BIN / File.basename(src)).to_s
          FileUtils.cp(src, dest); File.chmod(dest, 0o755)
          puts "-> #{dest}"
        end
      end
    end
  end

  {deps, build}
end

def install(name, src = false)
  return if installed?(name)
  puts "installing #{name}"
  url, bin = lookup(name)

  if !src && bin
    r = HTTP::Client.get(bin)
    abort "download failed" unless r.success?
    dest = (BIN / File.basename(bin)).to_s
    File.write(dest, r.body); File.chmod(dest, 0o755)
    puts "-> #{dest}"
    return
  end

  n    = url.split('/').last.gsub(/\.hg$|\.git$/, "")
  path = File.join(SRC, n)
  vcs  = url.includes?("hg") ? "hg" : "git"
  abort "clone failed" unless run(vcs, ["clone", url, path])

  deps, build_cmds = parse_molpkg(path)
  deps.each { |dep| install(dep) }

  if build_cmds.any?
    build_cmds.each do |cmd|
      parts = cmd.split
      abort "build failed: #{cmd}" unless run(parts[0], parts[1..], path)
    end
  else
    abort "no build steps for #{OS} in #{n}"
  end

  link_bins(path)
end

def remove_pkg(name)
  path = File.join(SRC, name)
  FileUtils.rm_rf(path) if Dir.exists?(path)
  ["", ".exe"].each do |ext|
    b = (BIN / "#{name}#{ext}").to_s
    File.delete(b) if File.exists?(b)
  end
  puts "removed #{name}"
end

def update_pkg(name)
  path = File.join(SRC, name)
  abort "#{name} not found, install first" unless Dir.exists?(path)
  vcs  = Dir.exists?(File.join(path, ".hg")) ? "hg" : "git"
  args = vcs == "hg" ? ["pull", "-u"] : ["pull"]
  abort "pull failed" unless run(vcs, args, path)
  _, build_cmds = parse_molpkg(path)
  build_cmds.each do |cmd|
    parts = cmd.split
    abort "build failed: #{cmd}" unless run(parts[0], parts[1..], path)
  end
  link_bins(path)
  puts "updated #{name}"
end

Dir.mkdir_p(SRC)
Dir.mkdir_p(File.dirname(DB))

case ARGV[0]?
when "install"
  name = ARGV[1]? || abort("usage: mol install <name> [--source]")
  src  = ARGV.includes?("--source")
  install(name, src)
when "remove"
  name = ARGV[1]? || abort("usage: mol remove <name>")
  remove_pkg(name)
when "update"
  name = ARGV[1]? || abort("usage: mol update <name>")
  update_pkg(name)
when "sync"
  r = HTTP::Client.get(REPO)
  abort "sync failed" unless r.success?
  File.write(DB, r.body); puts "synced"
when "-l"
  abort "no repo, run: mol sync" unless File.exists?(DB)
  File.each_line(DB) { |l| l=l.strip; puts l.split[0] unless l.empty? || l.starts_with?('#') }
else
  puts "mol as in molecule\n  install <name> [--source]  install a package\n  remove  <name>             remove a package\n  update  <name>             pull and rebuild\n  sync                       fetch package list\n  -l                         list packages"
end