# frozen_string_literal: true

require 'timeout'

module RedmineUxToolkit
  module Services
    class RepositoryRefresh
      DEFAULT_INTERVAL_MINUTES = 60
      DEFAULT_MAX_PER_RUN = 1
      DEFAULT_MAX_RUNTIME_SECONDS = 300
      INHERIT_INTERVAL_MINUTES = -1
      INTERVAL_OPTIONS = [0, 30, 60, 240, 1440].freeze
      REPOSITORY_INTERVAL_OPTIONS = [INHERIT_INTERVAL_MINUTES, *INTERVAL_OPTIONS].freeze
      MAX_PER_RUN_RANGE = 1..10
      MAX_RUNTIME_RANGE = 30..3600

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

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

        def default_interval_minutes(settings = Setting.plugin_redmine_ux_toolkit || {})
          interval = settings.fetch('repository_refresh_default_interval_minutes', DEFAULT_INTERVAL_MINUTES).to_i
          allowed_interval?(interval) ? interval : DEFAULT_INTERVAL_MINUTES
        end

        def max_per_run(settings = Setting.plugin_redmine_ux_toolkit || {})
          value = settings.fetch('repository_refresh_max_per_run', DEFAULT_MAX_PER_RUN).to_i
          MAX_PER_RUN_RANGE.cover?(value) ? value : DEFAULT_MAX_PER_RUN
        end

        def max_runtime_seconds(settings = Setting.plugin_redmine_ux_toolkit || {})
          value = settings.fetch('repository_refresh_max_runtime_seconds', DEFAULT_MAX_RUNTIME_SECONDS).to_i
          MAX_RUNTIME_RANGE.cover?(value) ? value : DEFAULT_MAX_RUNTIME_SECONDS
        end

        def failure_backoff_enabled?(settings = Setting.plugin_redmine_ux_toolkit || {})
          settings.fetch('repository_refresh_failure_backoff', '1').to_s == '1'
        end

        def allowed_interval?(value)
          INTERVAL_OPTIONS.include?(value.to_i)
        end

        def interval_options
          INTERVAL_OPTIONS
        end

        def repository_interval_options
          REPOSITORY_INTERVAL_OPTIONS
        end

        def allowed_repository_interval?(value)
          REPOSITORY_INTERVAL_OPTIONS.include?(value.to_i)
        end

        def table_available?
          UxToolkitRepositoryRefreshState.table_available?
        rescue NameError
          false
        end

        def repository_states
          return [] unless table_available?

          UxToolkitRepositoryRefreshState.includes(:repository, :project).order(:project_id, :repository_id).to_a
        end

        def repository_project_active?(repository)
          repository&.project&.active? == true
        end

        def due_count
          return 0 unless table_available?

          due_interval_states(active_repository_states.enabled).due.unlocked.count
        end

        def run_due(logger: Rails.logger)
          return skipped_result(I18n.t(:text_ux_toolkit_repository_refresh_status_unavailable)) unless table_available?
          return skipped_result(I18n.t(:text_ux_toolkit_repository_refresh_disabled)) unless enabled?

          processed = 0
          due_states.each do |state|
            result = refresh_state(state, logger: logger)
            processed += 1 unless result.status == 'locked'
          end

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

        def refresh_now(state_id, logger: Rails.logger)
          return skipped_result(I18n.t(:text_ux_toolkit_repository_refresh_status_unavailable)) unless table_available?

          state = UxToolkitRepositoryRefreshState.find_by(id: state_id)
          return Result.new(status: 'failed', message: I18n.t(:error_ux_toolkit_repository_refresh_state_not_found)) unless state
          return Result.new(status: 'failed', message: I18n.t(:error_ux_toolkit_repository_refresh_not_enabled)) unless state.enabled?
          return skipped_result(I18n.t(:text_ux_toolkit_repository_refresh_project_inactive)) unless state_project_active?(state)

          refresh_state(state, logger: logger, manual: true)
        end

        def status_label(status)
          case status.to_s
          when 'running' then I18n.t(:label_ux_toolkit_cron_status_running)
          when 'success' then I18n.t(:label_ux_toolkit_cron_status_success)
          when 'failed' then I18n.t(:label_ux_toolkit_cron_status_failed)
          else status.presence || '-'
          end
        end

        private

        def due_states
          due_interval_states(active_repository_states.enabled)
            .due
            .unlocked
            .order(:next_run_at, :id)
            .limit(max_per_run)
        end

        def refresh_state(state, logger:, manual: false)
          return skipped_result(I18n.t(:text_ux_toolkit_repository_refresh_project_inactive)) unless state_project_active?(state)

          claimed_state = state.claim!
          unless claimed_state
            return Result.new(status: 'locked', message: I18n.t(:error_ux_toolkit_repository_refresh_already_running))
          end

          repository = claimed_state.repository
          unless repository
            message = I18n.t(:error_ux_toolkit_repository_refresh_repository_missing)
            claimed_state.release_failure!(message, backoff_enabled: failure_backoff_enabled?)
            return Result.new(status: 'failed', message: message)
          end

          Timeout.timeout(max_runtime_seconds) { repository.fetch_changesets }
          message = I18n.t(:notice_ux_toolkit_repository_refresh_success, repository: claimed_state.repository_label)
          claimed_state.release_success!(message)
          Result.new(status: 'success', message: message)
        rescue Timeout::Error
          message = I18n.t(:error_ux_toolkit_repository_refresh_timeout)
          claimed_state&.release_failure!(message, backoff_enabled: failure_backoff_enabled?)
          Result.new(status: 'failed', message: message)
        rescue StandardError => e
          logger&.warn("[redmine_ux_toolkit] Repository refresh failed: #{e.class}: #{e.message}")
          message = "#{e.class}: #{e.message}"
          claimed_state&.release_failure!(message, backoff_enabled: failure_backoff_enabled?)
          Result.new(status: 'failed', message: message)
        end

        def skipped_result(message)
          Result.new(status: 'skipped', message: message)
        end

        def active_repository_states
          UxToolkitRepositoryRefreshState.joins(:project).merge(Project.active)
        end

        def due_interval_states(relation)
          if default_interval_minutes.positive?
            relation.where('refresh_interval_minutes > 0 OR refresh_interval_minutes = ?', INHERIT_INTERVAL_MINUTES)
          else
            relation.where('refresh_interval_minutes > 0')
          end
        end

        def state_project_active?(state)
          project = state.repository&.project || state.project
          project&.active? == true
        end
      end
    end
  end
end
