# frozen_string_literal: true

module RedmineUxToolkit
  module Cron
    class Runner
      LOCK_TIMEOUT = 30.minutes
      MESSAGE_LIMIT = 1000

      Result = Struct.new(:status, :message, keyword_init: true)
      Context = Struct.new(:started_at, :logger, keyword_init: true)

      class << self
        def register(name, job = nil, &block)
          registry[name.to_s] = job || block
        end

        def jobs
          registry.dup
        end

        private

        def registry
          @registry ||= {}
        end
      end

      def initialize(lock_timeout: LOCK_TIMEOUT, logger: Rails.logger)
        @lock_timeout = lock_timeout
        @logger = logger
        @lock_acquired = false
      end

      def run
        return locked_result unless acquire_lock

        started_at = Time.current
        context = Context.new(started_at: started_at, logger: logger)
        executed_jobs, failed_jobs = execute_jobs(context)
        if failed_jobs.any?
          message = "Cron run finished with failed jobs: #{failed_jobs.join('; ')}."
          finish('failed', message)
          Result.new(status: 'failed', message: message)
        else
          message = "Cron run finished. Jobs executed: #{executed_jobs.join(', ')}."
          finish('success', message)
          Result.new(status: 'success', message: message)
        end
      rescue StandardError => e
        logger&.error("[Redmine UX Toolkit] Cron run failed: #{e.class}: #{e.message}")
        finish('failed', "#{e.class}: #{e.message}") if lock_acquired?
        Result.new(status: 'failed', message: e.message)
      end

      private

      attr_reader :lock_timeout, :logger

      def acquire_lock
        state = UxToolkitCronState.default_record
        now = Time.current

        updated_rows = UxToolkitCronState
                       .where(id: state.id)
                       .where('locked_until IS NULL OR locked_until <= ?', now)
                       .update_all(
                         last_started_at: now,
                         last_status: 'running',
                         last_message: 'Cron run started.',
                         locked_until: now + lock_timeout,
                         updated_at: now
                       )

        @lock_acquired = updated_rows == 1
      end

      def execute_jobs(context)
        executed = []
        failed = []

        self.class.jobs.each do |name, job|
          result = execute_job(job, context)
          executed << name
          if result.respond_to?(:status) && result.status.to_s == 'failed'
            failed << "#{name}: #{result.respond_to?(:message) ? result.message : 'failed'}"
          end
        end

        [executed, failed]
      end

      def execute_job(job, context)
        if job.respond_to?(:call)
          job.call(context)
        elsif job.respond_to?(:perform)
          job.perform(context)
        else
          job.new.perform(context)
        end
      end

      def finish(status, message)
        UxToolkitCronState.default_record.update!(
          last_finished_at: Time.current,
          last_status: status,
          last_message: truncate_message(message),
          locked_until: nil
        )
      end

      def locked_result
        Result.new(
          status: 'locked',
          message: 'Another Redmine UX Toolkit cron run is already active.'
        )
      end

      def lock_acquired?
        @lock_acquired
      end

      def truncate_message(message)
        message.to_s.truncate(MESSAGE_LIMIT)
      end
    end

    Runner.register('noop', NoopJob)
    Runner.register('recurring_issues', Jobs::RecurringIssuesJob)
    Runner.register('issue_reminder_emails', Jobs::IssueReminderEmailsJob)
    if RedmineUxToolkit::Services::HelpdeskMailFetch.available?
      Runner.register('helpdesk_mail_fetch', Jobs::HelpdeskMailFetchJob)
    end
    Runner.register('repository_refresh', Jobs::RepositoryRefreshJob)
    Runner.register('attachment_trash_cleanup', Jobs::AttachmentTrashCleanupJob)
  end
end
