# typed: true
require 'fileutils'
require 'pathname'

module Kuby
  module Plugins
    module RailsApp
      # Works by maintaining a directory structure where each deploy creates
      # a new directory. Each directory is given a timestamped name. Assets
      # are symlinked into a special 'current' directory from each of the
      # timestamped directories. Each invocation overrides existing symlinks
      # if they already exist. This technique ensures assets from the previous
      # deploy remain available while the web servers are restarting.
      class AssetCopyTask
        TIMESTAMP_FORMAT = '%Y%m%d%H%M%S'.freeze
        KEEP = 5

        attr_reader :dest_path, :source_path

        def initialize(to:, from:)
          @dest_path = to
          @source_path = from
        end

        def run
          FileUtils.mkdir_p(ts_dir)
          FileUtils.mkdir_p(current_dir)

          copy_new_assets
          delete_old_assets

          nil
        end

        private

        def copy_new_assets
          # Copy all assets to new timestamp directory
          #
          # "source_path/." is special syntax. From the Ruby docs:
          # cp_r('src', 'dest') makes dest/src, but cp_r('src/.', 'dest') doesn't
          FileUtils.cp_r(File.join(source_path, '.'), ts_dir)

          relative_source_files = Dir.chdir(ts_dir) do
            Dir.glob(File.join('**', '*'))
          end

          relative_source_files.each do |relative_source_file|
            source_file = Pathname(File.join(current_dir, relative_source_file))
            source_ts_file = Pathname(File.join(ts_dir, relative_source_file))
            next unless File.file?(source_ts_file)

            # create individual symlinks for each file in source dir
            target_file = File.join(current_dir, relative_source_file)
            FileUtils.mkdir_p(File.dirname(target_file))
            source_ln_file = source_ts_file.relative_path_from(source_file.dirname)
            FileUtils.ln_s(source_ln_file, target_file, force: true)
            Kuby.logger.info("Linked #{source_ln_file} -> #{target_file}")
          end
        end

        def delete_old_assets
          # find all asset directories; directories have timestamp names
          asset_dirs = (Dir.glob(File.join(dest_path, '*')) - [current_dir])
            .select { |dir| File.directory?(dir) && try_parse_ts(File.basename(dir)) }
            .sort_by { |dir| parse_ts(File.basename(dir)) }
            .reverse

          # only keep the n most recent directories
          dirs_to_delete = asset_dirs[KEEP..-1] || []

          dirs_to_delete.each do |dir_to_delete|
            relative_files_to_delete = Dir.chdir(dir_to_delete) do
              Dir.glob(File.join('**', '*'))
            end

            relative_files_to_delete.each do |relative_file_to_delete|
              file_to_delete = File.join(dir_to_delete, relative_file_to_delete)
              next unless File.file?(file_to_delete)

              link = File.join(current_dir, relative_file_to_delete)
              next unless File.symlink?(link)

              # Only remove a symlink if it still points to a resource
              # in the directory we're currently deleting. Othewise, leave
              # it there - it was added by another deploy.
              if File.readlink(link) == file_to_delete
                File.unlink(link)
              end
            end

            FileUtils.rm_r(dir_to_delete)
          end
        end

        def try_parse_ts(ts)
          parse_ts(ts)
        rescue ArgumentError
          return nil
        end

        def parse_ts(ts)
          Time.strptime(ts, TIMESTAMP_FORMAT)
        end

        def ts_dir
          @ts_dir ||= File.join(
            dest_path, Time.now.strftime(TIMESTAMP_FORMAT)
          )
        end

        def current_dir
          @current_dir ||= File.join(dest_path, 'current')
        end
      end
    end
  end
end
