require 'digest'
require 'fileutils'
require 'json'
require 'listen'
require 'css_parser'
require 'jekyll-skyhook/tags'

module Jekyll
  class Skyhook
    class << self
      def instance(site)
        @instances ||= {}
        @instances[site] ||= new(site)
      end
    end
    private_class_method :new

    def initialize(site)
      @site = site
      skyhook_config = site.config['skyhook'] || {}
      @asset_dirs = Array(skyhook_config['asset_dirs'] || ['assets'])
      @digest_dir = skyhook_config['digest_dir'] || '_digested'
      @digested_path = File.join(site.source, @digest_dir)
      FileUtils.mkdir_p(@digested_path)

      # Configure image processing library
      @image_processor_library = skyhook_config['image_processor'] || 'mini_magick'
      load_image_processor

      # Load existing manifest if it exists
      manifest_dir = File.join(site.cache_dir, 'skyhook')
      manifest_path = File.join(manifest_dir, 'assets-manifest.json')
      @manifest = if File.exist?(manifest_path)
        JSON.parse(File.read(manifest_path))
      else
        {}
      end
    end

    def should_digest?
      skyhook_config = @site.config['skyhook'] || {}
      digest_assets = skyhook_config.fetch('digest_assets', true)
      env = @site.config['environment'] || 'development'

      case digest_assets
      when true
        true
      when false
        false
      when Array
        digest_assets.map(&:to_s).map(&:downcase).include?(env.downcase)
      else
        digest_assets.to_s.downcase == env.downcase
      end
    end

    def generate_digest(file_path)
      content = File.read(file_path)
      Digest::MD5.hexdigest(content)
    end

    def process_assets
      return unless should_digest?

      css_files = []

      @asset_dirs.each do |asset_dir|
        assets_dir = File.join(@site.source, asset_dir)
        next unless Dir.exist?(assets_dir)

        files = Dir.glob(File.join(assets_dir, '**', '*'))

        files.each do |file|
          next if File.directory?(file)

          if File.extname(file) == '.css'
            css_files << file
          else
            process_file(file)
          end
        end
      end

      # Process CSS files last so their URL rewriting can reference already-processed assets
      css_files.each do |file|
        process_file(file)
      end

      write_manifest
    end

    def process_file(file)
      digest = generate_digest(file)
      ext = File.extname(file)
      basename = File.basename(file, ext)
      original_relative_dir = File.dirname(file).sub(@site.source, '').sub(%r{^/}, '')

      digested_dir = File.join(@digested_path, original_relative_dir)
      FileUtils.mkdir_p(digested_dir)

      new_filename = "#{basename}-#{digest}#{ext}"
      new_filepath = File.join(digested_dir, new_filename)

      FileUtils.cp(file, new_filepath)

      static_file_dir = File.dirname(new_filepath).sub(@site.source, '').sub(%r{^/}, '')
      @site.static_files << Jekyll::StaticFile.new(
        @site,
        @site.source,
        static_file_dir,
        new_filename
      )

      original_relative_path = file.sub("#{@site.source}/", '')
      @manifest[original_relative_path] = File.join(@digest_dir, original_relative_dir, new_filename)

      rewrite_css_urls(new_filepath) if ext == '.css'
    end

    def process_image_transformation(file_path, transformations = {})
      return nil unless File.exist?(file_path)
      return nil unless transformations.any?

      original_relative_path = file_path.sub("#{@site.source}/", '')
      transform_key = build_transform_key(original_relative_path, transformations)

      return @manifest[transform_key] if @manifest.key?(transform_key)

      processed_image = apply_transformations(file_path, transformations)
      return nil unless processed_image

      result = store_transformed_image(original_relative_path, transformations, processed_image)

      # Write manifest after new transformation
      write_manifest

      result
    end

    def store_version(original_path, transformations, processed_image_path)
      original_relative_path = original_path.sub("#{@site.source}/", '')
      transform_key = build_transform_key(original_relative_path, transformations)

      content = File.read(processed_image_path)
      digest = generate_digest_from_content(content)

      ext = File.extname(processed_image_path)
      basename = File.basename(original_relative_path, File.extname(original_relative_path))
      transform_suffix = transformations.map { |k, v| "#{k}#{v}" }.sort.join('-')

      original_relative_dir = File.dirname(original_relative_path)
      digested_dir = File.join(@digested_path, original_relative_dir)
      FileUtils.mkdir_p(digested_dir)

      new_filename = "#{basename}-#{transform_suffix}-#{digest}#{ext}"
      new_filepath = File.join(digested_dir, new_filename)

      FileUtils.cp(processed_image_path, new_filepath)

      static_file_dir = File.dirname(new_filepath).sub(@site.source, '').sub(%r{^/}, '')
      @site.static_files << Jekyll::StaticFile.new(
        @site,
        @site.source,
        static_file_dir,
        new_filename
      )

      digested_relative_path = File.join(@digest_dir, original_relative_dir, new_filename)
      @manifest[transform_key] = digested_relative_path

      digested_relative_path
    end

    def write_manifest
      manifest_dir = File.join(@site.cache_dir, 'skyhook')
      FileUtils.mkdir_p(manifest_dir)
      manifest_path = File.join(manifest_dir, 'assets-manifest.json')
      File.write(manifest_path, JSON.pretty_generate(@manifest))
    end

    private

    def build_transform_key(original_path, transformations)
      params = transformations.map { |k, v| "#{k}=#{v}" }.sort.join('&')
      "#{original_path}##{params}"
    end

    def load_image_processor
      case @image_processor_library.to_s.downcase
      when 'vips'
        require 'image_processing/vips'
        @image_processor = ImageProcessing::Vips
      when 'mini_magick'
        require 'image_processing/mini_magick'
        @image_processor = ImageProcessing::MiniMagick
      else
        Jekyll.logger.warn "Skyhook:", "Unknown image_processor '#{@image_processor_library}'. Using mini_magick as fallback."
        require 'image_processing/mini_magick'
        @image_processor = ImageProcessing::MiniMagick
      end
    end

    def apply_transformations(file_path, transformations)
      processor = @image_processor.source(file_path)

      processor = processor.resize_to_limit(transformations[:width].to_i, nil) if transformations[:width]
      processor = processor.resize_to_limit(nil, transformations[:height].to_i) if transformations[:height]
      processor = processor.convert(transformations[:format]) if transformations[:format]

      processor
    end

    def store_transformed_image(original_relative_path, transformations, processed_image)
      ext = transformations[:format] ? ".#{transformations[:format]}" : File.extname(original_relative_path)
      basename = File.basename(original_relative_path, File.extname(original_relative_path))
      transform_suffix = transformations.map { |k, v| "#{k}#{v}" }.sort.join('-')

      original_relative_dir = File.dirname(original_relative_path)
      digested_dir = File.join(@digested_path, original_relative_dir)
      FileUtils.mkdir_p(digested_dir)

      # Process image and save to temporary location
      temp_result = processed_image.call

      # Calculate digest from processed content
      digest = generate_digest(temp_result.path)

      # Create final filename with digest
      new_filename = "#{basename}-#{transform_suffix}-#{digest}#{ext}"
      new_filepath = File.join(digested_dir, new_filename)

      # Move processed image to final location
      FileUtils.mv(temp_result.path, new_filepath)

      static_file_dir = File.dirname(new_filepath).sub(@site.source, '').sub(%r{^/}, '')
      @site.static_files << Jekyll::StaticFile.new(
        @site,
        @site.source,
        static_file_dir,
        new_filename
      )

      transform_key = build_transform_key(original_relative_path, transformations)
      digested_relative_path = File.join(@digest_dir, original_relative_dir, new_filename)
      @manifest[transform_key] = digested_relative_path

      digested_relative_path
    end

    def generate_digest_from_content(content)
      Digest::MD5.hexdigest(content)
    end

    def rewrite_css_urls(css_file)
      # Find the original CSS file path from the manifest
      original_css_path = nil
      @manifest.each do |key, value|
        if value == css_file.sub("#{@site.source}/", '')
          original_css_path = key
          break
        end
      end

      return unless original_css_path

      parser = CssParser::Parser.new
      parser.load_file!(css_file)

      parser.each_rule_set do |rule_set|
        rule_set.each_declaration do |property, value, _|
          next unless value.include?('url(')

          updated_value = value.gsub(/url\(['"]?(.*?)['"]?\)/) do |match|
            asset_path = $1
            next match if asset_path.start_with?('/') || asset_path.include?('://') || asset_path.start_with?('//')

            # Resolve relative path from original CSS location
            original_css_dir = File.dirname(original_css_path)
            resolved_asset_path = File.join(original_css_dir, asset_path)

            # Normalize path (remove ../ and ./)
            resolved_asset_path = File.expand_path(resolved_asset_path, '/').sub('/', '')


            if @manifest.key?(resolved_asset_path)
              "url(/#{@manifest[resolved_asset_path]})"
            else
              match
            end
          end
          rule_set[property] = updated_value
        end
      end

      File.write(css_file, parser.to_s)
    end
  end

  class SkyhookWatcher
    def initialize(site)
      @site = site
      @skyhook = Skyhook.instance(site)
      skyhook_config = site.config['skyhook'] || {}
      @asset_dirs = Array(skyhook_config['asset_dirs'] || ['assets'])
    end

    def start
      return if @listener&.listening?
      return unless @skyhook.should_digest?

      @asset_dirs.each do |asset_dir|
        assets_dir = File.join(@site.source, asset_dir)
        next unless Dir.exist?(assets_dir)

        digested_path = @skyhook.instance_variable_get(:@digested_path)

        @listener = Listen.to(assets_dir,
          ignore: %r{#{Regexp.escape(digested_path)}}) do |modified, added, removed|
          process_changes(added, modified, removed)
        end
        @listener.start
      end
    end

    private

    def process_changes(added, modified, removed)
      return if added.empty? && modified.empty? && removed.empty?

      added.each { |f| @skyhook.process_file(f) }
      modified.each { |f| @skyhook.process_file(f) }
      removed.each { |f| remove_from_manifest(f) }

      @skyhook.write_manifest
      puts "Processed #{added.size} new, #{modified.size} modified, #{removed.size} removed assets"
    end

    def remove_from_manifest(file)
      original_relative_path = file.sub("#{@site.source}/", '')
      @skyhook.instance_variable_get(:@manifest).delete(original_relative_path)
    end
  end
end

Jekyll::Hooks.register :site, :after_init do |site|
  skyhook = Jekyll::Skyhook.instance(site)
  skyhook.process_assets if skyhook.should_digest?
end

Jekyll::Hooks.register :site, :after_reset do |site|
  if site.config['serving']
    @skyhook_watcher ||= Jekyll::SkyhookWatcher.new(site).start
  end
end
