#!/usr/bin/env ruby

require 'base64'
require 'fileutils'
require 'json'
require 'net/http'
require 'pathname'
require 'tmpdir'
require 'uri'

SUPPORTED_EXTENSIONS = %w[.png .jpg .jpeg .webp].freeze

def format_bytes(bytes)
  format('%.2f MB', bytes.to_f / 1024 / 1024)
end

def collect_files(target)
  if File.file?(target)
    return SUPPORTED_EXTENSIONS.include?(File.extname(target).downcase) ? [target] : []
  end

  return [] unless Dir.exist?(target)

  Dir.glob(File.join(target, '**', '*'))
     .select { |path| File.file?(path) }
     .select { |path| SUPPORTED_EXTENSIONS.include?(File.extname(path).downcase) }
end

def build_http(uri)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.read_timeout = 120
  http.open_timeout = 30
  http
end

def tinypng_shrink(api_key, file_path)
  uri = URI('https://api.tinify.com/shrink')
  request = Net::HTTP::Post.new(uri)
  request['Authorization'] = "Basic #{Base64.strict_encode64("api:#{api_key}")}"
  request['Content-Type'] = 'application/octet-stream'
  request.body = File.binread(file_path)

  response = build_http(uri).request(request)
  unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
    message = begin
      body = JSON.parse(response.body)
      body['message'] || response.body
    rescue StandardError
      response.body
    end
    raise "Shrink failed (#{response.code}): #{message}"
  end

  JSON.parse(response.body)
end

def download_file(url, destination)
  uri = URI(url)
  request = Net::HTTP::Get.new(uri)
  response = build_http(uri).request(request)

  unless response.is_a?(Net::HTTPSuccess)
    raise "Download failed (#{response.code}): #{response.body}"
  end

  File.binwrite(destination, response.body)
end

def compress_file(api_key, file_path)
  original_size = File.size(file_path)
  shrink_result = tinypng_shrink(api_key, file_path)
  output = shrink_result.fetch('output')
  temp_path = File.join(Dir.tmpdir, "#{File.basename(file_path)}.tinypng_tmp")

  download_file(output.fetch('url'), temp_path)
  compressed_size = File.size(temp_path)
  FileUtils.mv(temp_path, file_path, force: true)

  {
    filePath: file_path,
    originalSize: original_size,
    compressedSize: compressed_size,
    savedBytes: original_size - compressed_size,
    ratio: output['ratio']
  }
ensure
  FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
end

api_key = ENV['TINYPNG_API_KEY']
abort('Missing TINYPNG_API_KEY environment variable') unless api_key && !api_key.empty?

retry_failed = ARGV.delete('--retry-failed')
targets = ARGV
abort('Usage: tinypng_batch.rb <dir> [dir...]') if targets.empty?

progress_path = File.expand_path('tinypng-progress.json')
existing_progress = if File.exist?(progress_path)
  JSON.parse(File.read(progress_path), symbolize_names: true)
else
  {}
end

results = Array(existing_progress[:results])
failures = Array(existing_progress[:failures])
processed_paths = results.map { |item| item[:filePath] }
processed_paths += failures.map { |item| item[:filePath] } unless retry_failed

target_files = targets.flat_map { |target| collect_files(File.expand_path(target)) }.uniq
files = target_files.reject { |path| processed_paths.include?(path) }
                    .sort_by { |path| -File.size(path) }

persist_progress = lambda do
  File.write(
    progress_path,
    JSON.pretty_generate(
      {
        results: results,
        failures: failures
      }
    ) + "\n"
  )
end

files.each_with_index do |file_path, index|
  begin
    result = compress_file(api_key, file_path)
    results << result
    persist_progress.call
    puts [
      'OK',
      index + 1,
      files.length,
      result[:savedBytes],
      result[:originalSize],
      result[:compressedSize],
      Pathname.new(file_path).relative_path_from(Pathname.pwd)
    ].join("\t")
  rescue StandardError => error
    failures << { filePath: file_path, message: error.message }
    persist_progress.call
    puts ['ERR', index + 1, files.length, Pathname.new(file_path).relative_path_from(Pathname.pwd), error.message].join("\t")
  end
end

summary = {
  totalFiles: target_files.length,
  trackedFiles: results.length + failures.length,
  compressedFiles: results.length,
  failedFiles: failures.length,
  originalBytes: results.sum { |item| item[:originalSize] },
  compressedBytes: results.sum { |item| item[:compressedSize] },
  savedBytes: results.sum { |item| item[:savedBytes] },
  failures: failures,
  topSavings: results.sort_by { |item| -item[:savedBytes] }.first(30)
}

report_path = File.expand_path('tinypng-report.json')
File.write(report_path, JSON.pretty_generate(summary) + "\n")

puts ['SUMMARY', summary[:totalFiles], summary[:trackedFiles], summary[:compressedFiles], summary[:failedFiles], summary[:originalBytes], summary[:compressedBytes], summary[:savedBytes]].join("\t")
puts ['REPORT', report_path].join("\t")
puts ['HUMAN', format_bytes(summary[:originalBytes]), format_bytes(summary[:compressedBytes]), format_bytes(summary[:savedBytes])].join("\t")
