require_dependency "mobile_detection"
require_dependency "crawler_detection"
require_dependency "guardian"

module Middleware
  class AnonymousCache

    def self.anon_cache(env, duration)
      env["ANON_CACHE_DURATION"] = duration
    end

    class Helper
      USER_AGENT = "HTTP_USER_AGENT".freeze
      RACK_SESSION = "rack.session".freeze
      ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze

      def initialize(env)
        @env = env
        @request = Rack::Request.new(@env)
      end

      def is_mobile=(val)
        @is_mobile = val ? :true : :false
      end

      def is_mobile?
        @is_mobile ||=
          begin
            session = @env[RACK_SESSION]
            # don't initialize params until later otherwise
            # you get a broken params on the request
            params = {}
            user_agent = @env[USER_AGENT]

            MobileDetection.resolve_mobile_view!(user_agent, params, session) ? :true : :false
          end

        @is_mobile == :true
      end

      def has_brotli?
        @has_brotli ||=
          begin
            @env[ACCEPT_ENCODING].to_s =~ /br/ ? :true : :false
          end
        @has_brotli == :true
      end

      def is_crawler?
        @is_crawler ||=
          begin
            user_agent = @env[USER_AGENT]
            CrawlerDetection.crawler?(user_agent) ? :true : :false
          end
        @is_crawler == :true
      end

      def cache_key
        @cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}|b=#{has_brotli?}|t=#{theme_key}"
      end

      def theme_key
        key, _ = @request.cookies['theme_key']&.split(',')
        if key && Guardian.new.allow_theme?(key)
          key
        else
          nil
        end
      end

      def cache_key_body
        @cache_key_body ||= "#{cache_key}_body"
      end

      def cache_key_other
        @cache_key_other || "#{cache_key}_other"
      end

      def get?
        @env["REQUEST_METHOD"] == "GET"
      end

      def has_auth_cookie?
        CurrentUser.has_auth_cookie?(@env)
      end

      def no_cache_bypass
        request = Rack::Request.new(@env)
        request.cookies['_bypass_cache'].nil?
      end

      def cacheable?
        !!(!has_auth_cookie? && get? && no_cache_bypass)
      end

      def cached
        if body = $redis.get(cache_key_body)
          if other = $redis.get(cache_key_other)
            other = JSON.parse(other)
            [other[0], other[1], [body]]
          end
        end
      end

      def cache_duration
        @env["ANON_CACHE_DURATION"]
      end

      # NOTE in an ideal world cache still serves out cached content except for one magic worker
      #  that fills it up, this avoids a herd killing you, we can probably do this using a job or redis tricks
      #  but coordinating this is tricky
      def cache(result)
        status, headers, response = result

        if status == 200 && cache_duration
          headers_stripped = headers.dup.delete_if { |k, _| ["Set-Cookie", "X-MiniProfiler-Ids"].include? k }
          headers_stripped["X-Discourse-Cached"] = "true"
          parts = []
          response.each do |part|
            parts << part
          end

          $redis.setex(cache_key_body,  cache_duration, parts.join)
          $redis.setex(cache_key_other, cache_duration, [status, headers_stripped].to_json)
        else
          parts = response
        end

        [status, headers, parts]
      end

      def clear_cache
        $redis.del(cache_key_body)
        $redis.del(cache_key_other)
      end

    end

    def initialize(app, settings = {})
      @app = app
    end

    def call(env)
      helper = Helper.new(env)

      if helper.cacheable?
        helper.cached || helper.cache(@app.call(env))
      else
        @app.call(env)
      end

    end

  end

end