# frozen_string_literal: true

dest = File.absolute_path('sass', ENV.fetch('RUBYARCHDIR', File.absolute_path('../../lib', __dir__)))
FileUtils.mkdir_p(dest)
Dir.chdir(dest)

require 'rake/clean'

require_relative '../../lib/sass/elf'

ELF = Sass.const_get(:ELF)

task default: %i[install clean]

task install: %w[cli.rb]

CLEAN.include %w[
  protoc.exe
  ruby
  *.proto
  *.tar.gz
  *.zip
]

CLOBBER.include %w[
  dart-sass
  cli.rb
  embedded_sass_pb.rb
  node_modules
  bun.lockb
  package-lock.json
  pnpm-lock.yaml
  yarn.lock
]

file 'protoc.exe' do |t|
  fetch(SassConfig.protoc, t.name)
  chmod 'a+x', t.name
rescue NotImplementedError
  File.write(t.name, <<~PROTOC_EXE)
    #!#{RbConfig.ruby}
    # frozen_string_literal: true
    Kernel.exec('protoc', *ARGV)
  PROTOC_EXE
  chmod 'a+x', t.name
end

file 'dart-sass/sass' do
  gem_install 'sass-embedded', SassConfig.gem_version, SassConfig.gem_platform do |installer|
    gh_attestation_verify(installer.gem, repo: 'sass-contrib/sass-embedded-host-ruby')
    mv File.absolute_path('lib/sass/dart-sass', installer.gem_dir), 'dart-sass'
  end
rescue StandardError
  archive = fetch(SassConfig.dart_sass)
  gh_attestation_verify(archive, repo: 'sass/dart-sass')
  unarchive archive
  rm archive
end

file 'node_modules/sass' do
  cp File.absolute_path('package.json', __dir__), './'

  # TODO: remove after https://github.com/sass/dart-sass/pull/2413
  cp File.absolute_path("sass-#{SassConfig.dart_sass_version}.tgz", __dir__), './'

  begin
    sh 'npm', 'install'
  rescue StandardError
    begin
      sh 'yarn', 'install'
    rescue StandardError
      begin
        sh 'pnpm', 'install'
      rescue StandardError
        sh 'bun', 'install'
      end
    end
  end
end

task 'dart-sass' do
  Rake::Task['dart-sass/sass'].invoke
rescue NotImplementedError
  Rake::Task['node_modules/sass'].invoke
end

file 'cli.rb' => %w[dart-sass] do |t|
  begin
    exe = 'dart-sass/sass'
    exe = "#{exe}#{['', '.bat', '.exe'].find { |ext| File.exist?("#{exe}#{ext}") }}"

    raise Errno::ENOENT, exe unless File.exist?(exe)

    runtime = 'dart-sass/src/dart'
    runtime = "#{runtime}#{['', '.exe'].find { |ext| File.exist?("#{runtime}#{ext}") }}"
    snapshot = 'dart-sass/src/sass.snapshot'

    command = if File.exist?(runtime) && File.exist?(snapshot)
                [runtime, snapshot]
              else
                [exe]
              end

    interpreter = File.open(command[0], 'rb') do |file|
      ELF.new(file).interpreter
    rescue ArgumentError
      nil
    end

    command_source = command.map do |argument|
      "File.absolute_path('#{argument}', __dir__).freeze"
    end.join(',
      ')
  rescue Errno::ENOENT
    package = 'node_modules/sass'

    script = File.join(package, SassConfig.package_json(package)['bin']['sass'])

    interpreter = nil

    command_source = [
      "'node'",
      "File.absolute_path('#{script}', __dir__).freeze"
    ].join(',
      ')
  end

  if interpreter.nil?
    File.write(t.name, <<~CLI_RB)
      # frozen_string_literal: true

      module Sass
        module CLI
          COMMAND = [
            #{command_source}
          ].freeze
        end

        private_constant :CLI
      end
    CLI_RB
  else
    File.write(t.name, <<~CLI_RB)
      # frozen_string_literal: true

      require 'sass/elf'

      module Sass
        module CLI
          INTERPRETER = '#{interpreter}'

          INTERPRETER_SUFFIX = '/#{File.basename(interpreter)}'

          COMMAND = [
            *(ELF::INTERPRETER if ELF::INTERPRETER != INTERPRETER && ELF::INTERPRETER&.end_with?(INTERPRETER_SUFFIX)),
            #{command_source}
          ].freeze
        end

        private_constant :CLI
      end
    CLI_RB
  end
end

file 'embedded_sass.proto' => %w[cli.rb] do |t|
  fetch(SassConfig.embedded_sass_protocol, t.name)
end

rule '_pb.rb' => %w[.proto protoc.exe] do |t|
  sh './protoc.exe', '--proto_path=.', '--ruby_out=.', t.source
end

rule '_pb.rbs' => %w[.proto protoc.exe] do |t|
  sh './protoc.exe', '--proto_path=.', '--rbs_out=../../sig/sass', t.source
end

# rubocop:disable Style/OneClassPerFile

# This is a FileUtils extension that defines several additional commands to be
# added to the FileUtils utility functions.
module FileUtils
  def unarchive(archive)
    if Gem.win_platform?
      sh File.absolute_path('tar.exe', Utils.windows_system_directory), '-vxf', archive
    elsif archive.downcase.end_with?('.zip')
      sh 'unzip', '-o', archive
    else
      sh 'tar', '-vxf', archive, '--no-same-owner', '--no-same-permissions'
    end
  end

  def fetch(source_uri, dest_path = nil)
    dest_path = File.basename(source_uri) if dest_path.nil?

    Rake.rake_output_message "fetch #{source_uri}" if Rake::FileUtilsExt.verbose_flag

    unless Rake::FileUtilsExt.nowrite_flag
      data = Utils.fetch_https(source_uri)
      Gem.write_binary(dest_path, data)
    end

    dest_path
  end

  def gem_install(name, version, platform)
    require 'rubygems/remote_fetcher'

    install_dir = File.absolute_path('ruby')

    if Rake::FileUtilsExt.verbose_flag
      Rake.rake_output_message [
        'gem', 'install',
        '--force',
        '--install-dir', install_dir,
        '--no-document', '--ignore-dependencies',
        '--platform', platform,
        '--version', version,
        'sass-embedded'
      ].join(' ')
    end

    dependency = Gem::Dependency.new(name, version)

    dependency_request = Gem::Resolver::DependencyRequest.new(dependency, nil)

    resolver_spec = Gem::Resolver::BestSet.new.find_all(dependency_request).find do |s|
      s.platform == platform
    end

    raise Gem::UnsatisfiableDependencyError, dependency_request if resolver_spec.nil?

    options = { force: true, install_dir: }
    if Rake::FileUtilsExt.nowrite_flag
      installer = Gem::Installer.for_spec(resolver_spec.spec, options)
    else
      path = resolver_spec.download(options)
      installer = Gem::Installer.at(path, options)
      installer.install
    end

    yield installer
  ensure
    rm_rf install_dir unless Rake::FileUtilsExt.nowrite_flag
  end

  def gh_attestation_verify(path, repo:, hostname: 'github.com')
    if SassConfig.development? && system('gh', 'auth', 'status', '--hostname', hostname, %i[out err] => File::NULL)
      sh 'gh', 'attestation', 'verify', path, '--hostname', hostname, '--repo', repo
    end
  end
end

# The {Platform} module.
module Platform
  HOST_CPU = RbConfig::CONFIG['host_cpu'].downcase

  CPU = case HOST_CPU
        when /amd64|x86_64|x64/
          'x86_64'
        when /i\d86|x86|i86pc/
          'i386'
        when /arm64|aarch64/
          'aarch64'
        when /arm/
          'arm'
        when /ppc64le|powerpc64le/
          'ppc64le'
        else
          HOST_CPU
        end

  HOST_OS = RbConfig::CONFIG['host_os'].downcase

  OS = case HOST_OS
       when /darwin/
         'darwin'
       when /linux-android/
         'linux-android'
       when /linux-musl/
         'linux-musl'
       when /linux-none/
         'linux-none'
       when /linux-uclibc/
         'linux-uclibc'
       when /linux/
         'linux'
       when *Gem::WIN_PATTERNS
         'windows'
       else
         HOST_OS
       end

  ARCH = "#{CPU}-#{OS}".freeze
end

# The {SassConfig} module.
module SassConfig
  module_function

  def package_json(path = '.')
    require 'json'

    JSON.parse(File.read(File.absolute_path('package.json', path)))
  end

  def dart_sass_version
    package_json(__dir__)['dependencies']['sass']
      # TODO: remove after https://github.com/sass/dart-sass/pull/2413
      .delete_prefix('file:sass-').delete_suffix('.tgz')
  end

  def dart_sass
    repo = 'https://github.com/sass/dart-sass'

    tag_name = dart_sass_version

    message = "dart-sass for #{Platform::ARCH} not available at #{repo}/releases/tag/#{tag_name}"

    env = ''

    os = case Platform::OS
         when 'darwin'
           'macos'
         when 'linux'
           'linux'
         when 'linux-android'
           'android'
         when 'linux-musl'
           env = '-musl'
           'linux'
         when 'windows'
           'windows'
         else
           raise NotImplementedError, message
         end

    cpu = case Platform::CPU
          when 'x86_64'
            'x64'
          when 'aarch64'
            'arm64'
          when 'arm'
            'arm'
          when 'riscv64'
            'riscv64'
          else
            raise NotImplementedError, message
          end

    ext = Platform::OS == 'windows' ? 'zip' : 'tar.gz'

    "#{repo}/releases/download/#{tag_name}/dart-sass-#{tag_name}-#{os}-#{cpu}#{env}.#{ext}"
  end

  def protoc
    repo = 'https://repo.maven.apache.org/maven2/com/google/protobuf/protoc'

    dependency = Gem::Dependency.new('google-protobuf')

    spec = dependency.to_spec

    version = spec.version

    message = "protoc for #{Platform::ARCH} not available at #{repo}/#{version}"

    os = case Platform::OS
         when 'darwin'
           'osx'
         when 'linux', 'linux-android', 'linux-musl', 'linux-none', 'linux-uclibc'
           'linux'
         when 'windows'
           'windows'
         else
           raise NotImplementedError, message
         end

    cpu = case Platform::CPU
          when 'i386'
            'x86_32'
          when 'x86_64'
            'x86_64'
          when 'aarch64'
            Platform::OS == 'windows' ? 'x86_64' : 'aarch_64'
          when 'ppc64le'
            'ppcle_64'
          when 's390x'
            's390_64'
          else
            raise NotImplementedError, message
          end

    uri = "#{repo}/#{version}/protoc-#{version}-#{os}-#{cpu}.exe"

    Utils.fetch_https("#{uri}.sha1")

    uri
  rescue Gem::RemoteFetcher::FetchError
    dependency_request = Gem::Resolver::DependencyRequest.new(dependency, nil)

    versions = Gem::Resolver::BestSet.new.find_all(dependency_request).filter_map do |s|
      s.version if s.platform == Gem::Platform::RUBY
    end

    versions.sort.reverse_each do |v|
      uri = "#{repo}/#{v}/protoc-#{v}-#{os}-#{cpu}.exe"

      Utils.fetch_https("#{uri}.sha1")

      return uri
    rescue Gem::RemoteFetcher::FetchError
      next
    end

    raise NotImplementedError, message
  end

  def embedded_sass_protocol
    require 'json'

    version = Utils.capture(RbConfig.ruby,
                            File.absolute_path('../../exe/sass', __dir__),
                            '--embedded',
                            '--version')

    tag_name = JSON.parse(version)['protocolVersion']

    "https://github.com/sass/sass/raw/embedded-protocol-#{tag_name}/spec/embedded_sass.proto"
  end

  def development?
    File.exist?('../../Gemfile')
  end

  def gem_version
    require_relative '../../lib/sass/embedded/version'

    development? ? dart_sass_version : Sass::Embedded::VERSION
  end

  def gem_platform
    platform = Gem::Platform.new("#{Platform::CPU}-#{Platform::HOST_OS}")
    case Platform::OS
    when 'darwin'
      case platform.cpu
      when 'aarch64'
        Gem::Platform.new(['arm64', platform.os])
      else
        platform
      end
    when 'linux'
      if platform.version&.start_with?('gnu')
        platform
      else
        Gem::Platform.new([platform.cpu, platform.os, "gnu#{platform.version}"])
      end
    when 'windows'
      case platform.cpu
      when 'x86_64'
        Gem::Platform.new('x64-mingw-ucrt')
      else
        Gem::Platform.new([platform.cpu, 'mingw', 'ucrt'])
      end
    else
      platform
    end
  end
end

# The {Utils} module.
module Utils
  module_function

  def capture(...)
    require 'open3'

    stdout, stderr, status = Open3.capture3(...)

    raise stderr unless status.success?

    stdout
  end

  def fetch_https(source_uri)
    require 'rubygems/remote_fetcher'

    source_uri = begin
      Gem::Uri.parse!(source_uri)
    rescue NoMethodError
      URI.parse(source_uri)
    end

    Gem::RemoteFetcher.fetcher.fetch_https(source_uri)
  end

  def windows_system_directory
    path = capture('powershell.exe',
                   '-NoLogo',
                   '-NoProfile',
                   '-NonInteractive',
                   '-Command',
                   '[Environment]::GetFolderPath([Environment+SpecialFolder]::System) | Write-Host -NoNewline')

    File.absolute_path(path)
  end
end

# rubocop:enable Style/OneClassPerFile
