require "rbconfig"
require "open-uri"
require "zlib"
require "open3"

module Javy
  ROOT = __dir__
  BIN_FOLDER = File.join(ROOT, "bin")
  VERSION = File.read(File.join(ROOT, "version")).strip
  TARGET = File.join(BIN_FOLDER, "javy-#{VERSION}")

  class << self
    def install
      ShopifyCLI::Result
        .wrap { Installer.call(target: target, platform: platform, version: VERSION) }
        .call
    end

    def build(source:, dest: nil)
      ensure_installed

      optional_args = []
      optional_args += ["-o", dest] unless dest.nil?

      ShopifyCLI::Result
        .wrap { exec(source, *optional_args) }
        .call
    end

    private

    def exec(*args, **kwargs)
      out_and_err, stat = CLI::Kit::System.capture2e(target, *args, **kwargs)
      raise ExecutionError, out_and_err unless stat.success?
      true
    end

    def platform
      Platform.new
    end

    def target
      platform.format_executable_path(TARGET)
    end

    def ensure_installed
      delete_outdated_installations
      install unless Installer.installed?(target: target)
    end

    def delete_outdated_installations
      installed_binaries
        .reject { |v| v == target }
        .each { |file| File.delete(file) }
    end

    def installed_binaries
      Dir[File.join(BIN_FOLDER, "javy-*")]
    end

    module Installer
      def self.call(target:, platform:, version:)
        asset = Asset.new(
          platform: platform,
          version: version,
          owner: "Shopify",
          repository: "javy",
          basename: "javy"
        )

        downloaded = asset.download(target: target)
        raise InstallationError.asset_not_found(platform: platform, version: version, url: asset.url) unless downloaded

        true
      end

      def self.installed?(target:)
        File.executable?(target)
      end
    end
  end

  class Error < RuntimeError; end
  class ExecutionError < Error; end

  class InstallationError < Error
    def self.cpu_unsupported
      new("Javy is not supported on this CPU")
    end

    def self.asset_not_found(platform:, version:, url:)
      new(format(
        "Unable to download javy %{version} for %{os} (%{cpu}) at %{url}",
        version: version,
        os: platform.os,
        cpu: platform.cpu,
        url: url
      ))
    end
  end

  Asset = Struct.new(:platform, :version, :owner, :repository, :basename, keyword_init: true) do
    def download(target:)
      FileUtils.mkdir_p(BIN_FOLDER)

      Dir.chdir(File.dirname(target)) do
        File.open(File.basename(target), "wb") do |target_file|
          decompress(url.open, target_file)
        end
        File.chmod(0755, target)
      end

      true
    rescue OpenURI::HTTPError
      false
    end

    def url
      URI.parse(format(
        "https://github.com/%{owner}/%{repository}/releases/download/%{version}/%{filename}",
        owner: owner,
        repository: repository,
        version: version,
        filename: filename
      ))
    end

    def filename
      format(
        "%{basename}-%{cpu}-%{os}-%{version}.gz",
        basename: basename,
        cpu: platform.cpu,
        os: platform.os,
        version: version
      )
    end

    private

    def decompress(source, target)
      zlib = Zlib::GzipReader.new(source)
      target << zlib.read
    ensure
      zlib.close
    end
  end

  Platform = Struct.new(:ruby_config) do
    def initialize(ruby_config = RbConfig::CONFIG)
      super(ruby_config)
    end

    def to_s
      format("%{cpu}-%{os}", cpu: cpu, os: os)
    end

    def os
      case ruby_config.fetch("host_os")
      when /linux/
        "linux"
      when /darwin/
        "macos"
      else
        "windows"
      end
    end

    def cpu
      case ruby_config.fetch("host_cpu")
      when "x64", "x86_64"
        "x86_64"
      else
        raise InstallationError.cpu_unsupported
      end
    end

    def format_executable_path(path)
      case os
      when "windows"
        File.extname(path) != ".exe" ? path + ".exe" : path
      else
        path
      end
    end
  end
end