# frozen_string_literal: true

require 'fileutils'

module RedmineUxToolkit
  module Services
    class AttachmentStorage
      DEFAULT_RETENTION_DAYS = 30
      DEFAULT_CLEANUP_LIMIT = 50
      RETENTION_RANGE = 1..3650
      CLEANUP_LIMIT_RANGE = 1..500
      TRASH_DIRECTORY_NAME = 'redmine_ux_toolkit_trash'

      Result = Struct.new(:status, :message, :count, keyword_init: true)

      class << self
        def enabled?(settings = Setting.plugin_redmine_ux_toolkit || {})
          settings.fetch('enable_attachment_trash', '0').to_s == '1'
        end

        def retention_days(settings = Setting.plugin_redmine_ux_toolkit || {})
          value = settings.fetch('attachment_trash_retention_days', DEFAULT_RETENTION_DAYS).to_i
          RETENTION_RANGE.cover?(value) ? value : DEFAULT_RETENTION_DAYS
        end

        def cleanup_limit(settings = Setting.plugin_redmine_ux_toolkit || {})
          value = settings.fetch('attachment_trash_cleanup_limit', DEFAULT_CLEANUP_LIMIT).to_i
          CLEANUP_LIMIT_RANGE.cover?(value) ? value : DEFAULT_CLEANUP_LIMIT
        end

        def table_available?
          UxToolkitAttachmentTrashRecord.table_available?
        rescue NameError
          false
        end

        def trash_root
          Rails.root.join('files', TRASH_DIRECTORY_NAME).to_s
        end

        def attachment_status(attachment, trash_record = nil)
          return 'in_trash' if trash_record

          path = attachment_path(attachment)
          File.exist?(path) ? 'active' : 'missing'
        end

        def move_to_trash(attachment_ids, user:)
          return failed_result(I18n.t(:error_ux_toolkit_attachment_storage_disabled)) unless enabled?
          return failed_result(I18n.t(:text_ux_toolkit_attachment_storage_unavailable)) unless table_available?

          selected_ids = normalize_ids(attachment_ids)
          return failed_result(I18n.t(:error_ux_toolkit_attachment_storage_no_selection)) if selected_ids.blank?

          count = 0
          Attachment.where(id: selected_ids).find_each do |attachment|
            move_attachment_to_trash(attachment, user)
            count += 1
          end

          success_result(I18n.t(:notice_ux_toolkit_attachment_storage_moved_to_trash, count: count), count)
        rescue StandardError => e
          failed_result(e.message, count)
        end

        def restore(record_ids, user:)
          return failed_result(I18n.t(:text_ux_toolkit_attachment_storage_unavailable)) unless table_available?

          selected_ids = normalize_ids(record_ids)
          return failed_result(I18n.t(:error_ux_toolkit_attachment_storage_no_selection)) if selected_ids.blank?

          count = 0
          UxToolkitAttachmentTrashRecord.trashed.where(id: selected_ids).find_each do |record|
            restore_record(record, user)
            count += 1
          end

          success_result(I18n.t(:notice_ux_toolkit_attachment_storage_restored, count: count), count)
        rescue StandardError => e
          failed_result(e.message, count)
        end

        def permanently_delete(record_ids, user:)
          return failed_result(I18n.t(:text_ux_toolkit_attachment_storage_unavailable)) unless table_available?

          selected_ids = normalize_ids(record_ids)
          return failed_result(I18n.t(:error_ux_toolkit_attachment_storage_no_selection)) if selected_ids.blank?

          count = 0
          UxToolkitAttachmentTrashRecord.trashed.where(id: selected_ids).find_each do |record|
            permanently_delete_record(record, user)
            count += 1
          end

          success_result(I18n.t(:notice_ux_toolkit_attachment_storage_permanently_deleted, count: count), count)
        rescue StandardError => e
          failed_result(e.message, count)
        end

        def cleanup_expired(logger: Rails.logger)
          return Result.new(status: 'skipped', message: I18n.t(:error_ux_toolkit_attachment_storage_disabled), count: 0) unless enabled?
          return Result.new(status: 'skipped', message: I18n.t(:text_ux_toolkit_attachment_storage_unavailable), count: 0) unless table_available?

          count = 0
          UxToolkitAttachmentTrashRecord.trashed.expired.order(:expires_at, :id).limit(cleanup_limit).find_each do |record|
            permanently_delete_record(record, nil)
            count += 1
          rescue StandardError => e
            logger&.warn("[redmine_ux_toolkit] Attachment trash cleanup failed: #{e.class}: #{e.message}")
            record.update(last_message: record.truncate_message(e.message))
          end

          Result.new(
            status: 'success',
            message: I18n.t(:notice_ux_toolkit_attachment_storage_cleanup_processed, count: count),
            count: count
          )
        end

        def attachment_path(attachment)
          attachment.respond_to?(:diskfile) ? attachment.diskfile.to_s : File.join(Attachment.storage_path, attachment.disk_directory.to_s, attachment.disk_filename.to_s)
        end

        private

        def move_attachment_to_trash(attachment, user)
          active_record = UxToolkitAttachmentTrashRecord.trashed.find_by(attachment_id: attachment.id)
          return active_record if active_record

          source = safe_active_path(attachment_path(attachment))
          raise I18n.t(:error_ux_toolkit_attachment_storage_file_missing) unless File.file?(source)

          FileUtils.mkdir_p(trash_root)
          destination = File.join(trash_root, "#{attachment.id}-#{attachment.disk_filename}")
          raise I18n.t(:error_ux_toolkit_attachment_storage_trash_exists) if File.exist?(destination)

          FileUtils.mv(source, destination)
          UxToolkitAttachmentTrashRecord.create!(
            attachment_id: attachment.id,
            original_disk_filename: attachment.disk_filename.to_s,
            original_path: source,
            trash_path: destination,
            original_filename: attachment.filename.to_s,
            file_size: attachment.filesize,
            content_type: attachment.content_type,
            container_type: attachment.container_type,
            container_id: attachment.container_id,
            moved_to_trash_by: user,
            moved_to_trash_at: Time.current,
            expires_at: Time.current + retention_days.days,
            status: UxToolkitAttachmentTrashRecord::STATUS_TRASHED,
            last_message: I18n.t(:notice_ux_toolkit_attachment_storage_file_moved)
          )
        end

        def restore_record(record, user)
          source = safe_trash_path(record.trash_path)
          destination = safe_active_path(record.original_path)

          unless File.file?(source)
            record.mark_missing!(I18n.t(:error_ux_toolkit_attachment_storage_trash_file_missing))
            raise I18n.t(:error_ux_toolkit_attachment_storage_trash_file_missing)
          end

          raise I18n.t(:error_ux_toolkit_attachment_storage_restore_destination_exists) if File.exist?(destination)

          FileUtils.mkdir_p(File.dirname(destination))
          FileUtils.mv(source, destination)
          record.update!(
            restored_by: user,
            restored_at: Time.current,
            status: UxToolkitAttachmentTrashRecord::STATUS_RESTORED,
            last_message: I18n.t(:notice_ux_toolkit_attachment_storage_file_restored)
          )
        end

        def permanently_delete_record(record, user)
          source = safe_trash_path(record.trash_path)
          FileUtils.rm_f(source) if File.exist?(source)
          record.update!(
            permanently_deleted_by: user,
            permanently_deleted_at: Time.current,
            status: UxToolkitAttachmentTrashRecord::STATUS_DELETED,
            last_message: I18n.t(:notice_ux_toolkit_attachment_storage_file_deleted)
          )
        end

        def safe_active_path(path)
          safe_path_under(path, Attachment.storage_path.to_s)
        end

        def safe_trash_path(path)
          safe_path_under(path, trash_root)
        end

        def safe_path_under(path, root)
          expanded_path = File.expand_path(path.to_s)
          expanded_root = File.expand_path(root.to_s)
          unless expanded_path == expanded_root || expanded_path.start_with?(expanded_root + File::SEPARATOR)
            raise I18n.t(:error_ux_toolkit_attachment_storage_unsafe_path)
          end

          expanded_path
        end

        def normalize_ids(ids)
          Array(ids).map(&:to_s).select {|id| id.match?(/\A\d+\z/) }.map(&:to_i).uniq
        end

        def success_result(message, count)
          Result.new(status: 'success', message: message, count: count)
        end

        def failed_result(message, count = 0)
          Result.new(status: 'failed', message: message, count: count)
        end
      end
    end
  end
end
