require 'current_user'
require_dependency 'canonical_url'
require_dependency 'discourse'
require_dependency 'custom_renderer'
require_dependency 'archetype'
require_dependency 'rate_limiter'

class ApplicationController < ActionController::Base
  include CurrentUser
  include CanonicalURL::ControllerExtensions

  serialization_scope :guardian

  protect_from_forgery

  # Default Rails 3.2 lets the request through with a blank session
  #  we are being more pedantic here and nulling session / current_user
  #  and then raising a CSRF exception
  def handle_unverified_request
    # NOTE: API key is secret, having it invalidates the need for a CSRF token
    unless is_api?
      super
      clear_current_user
      render text: "['BAD CSRF']", status: 403
    end
  end

  before_filter :set_locale
  before_filter :set_mobile_view
  before_filter :inject_preview_style
  before_filter :disable_customization
  before_filter :block_if_maintenance_mode
  before_filter :authorize_mini_profiler
  before_filter :store_incoming_links
  before_filter :preload_json
  before_filter :check_xhr
  before_filter :redirect_to_login_if_required

  rescue_from Exception do |exception|
    unless [ActiveRecord::RecordNotFound,
            ActionController::RoutingError,
            ActionController::UnknownController,
            AbstractController::ActionNotFound].include? exception.class
      begin
        ErrorLog.report_async!(exception, self, request, current_user)
      rescue
        # dont care give up
      end
    end
    raise
  end


  # Some exceptions
  class RenderEmpty < Exception; end

  # Render nothing unless we are an xhr request
  rescue_from RenderEmpty do
    render 'default/empty'
  end

  # If they hit the rate limiter
  rescue_from RateLimiter::LimitExceeded do |e|

    time_left = ""
    if e.available_in < 1.minute.to_i
      time_left = I18n.t("rate_limiter.seconds", count: e.available_in)
    elsif e.available_in < 1.hour.to_i
      time_left = I18n.t("rate_limiter.minutes", count: (e.available_in / 1.minute.to_i))
    else
      time_left = I18n.t("rate_limiter.hours", count: (e.available_in / 1.hour.to_i))
    end

    render json: {errors: [I18n.t("rate_limiter.too_many_requests", time_left: time_left)]}, status: 429
  end

  rescue_from Discourse::NotLoggedIn do |e|
    raise e if Rails.env.test?
    redirect_to "/"
  end

  rescue_from Discourse::NotFound do
    rescue_discourse_actions("[error: 'not found']", 404) # TODO: this breaks json responses
  end

  rescue_from Discourse::InvalidAccess do
    rescue_discourse_actions("[error: 'invalid access']", 403) # TODO: this breaks json responses
  end

  def rescue_discourse_actions(message, error)
    if request.format && request.format.json?
      # TODO: this doesn't make sense. Stuffing an html page into a json response will cause
      #       $.parseJSON to fail in the browser. Also returning text like "[error: 'invalid access']"
      #       from the above rescue_from blocks will fail because that isn't valid json.
      render status: error, layout: false, text: (error == 404) ? build_not_found_page(error) : message
    else
      render text: build_not_found_page(error, 'no_js')
    end
  end

  def set_locale
    I18n.locale = SiteSetting.default_locale
  end

  def store_preloaded(key, json)
    @preloaded ||= {}
    # I dislike that there is a gsub as opposed to a gsub!
    #  but we can not be mucking with user input, I wonder if there is a way
    #  to inject this safty deeper in the library or even in AM serializer
    @preloaded[key] = json.gsub("</", "<\\/")
  end

  # If we are rendering HTML, preload the session data
  def preload_json
    # We don't preload JSON on xhr or JSON request
    return if request.xhr?

    preload_anonymous_data

    if current_user
      preload_current_user_data
      current_user.sync_notification_channel_position
    end
  end

  def set_mobile_view
    session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view)
  end

  def inject_preview_style
    style = request['preview-style']
    session[:preview_style] = style if style
  end

  def disable_customization
    session[:disable_customization] = params[:customization] == "0" if params.has_key?(:customization)
  end

  def guardian
    @guardian ||= Guardian.new(current_user)
  end

  def serialize_data(obj, serializer, opts={})
    # If it's an array, apply the serializer as an each_serializer to the elements
    serializer_opts = {scope: guardian}.merge!(opts)
    if obj.is_a?(Array) or obj.is_a?(ActiveRecord::Associations::CollectionProxy)
      serializer_opts[:each_serializer] = serializer
      ActiveModel::ArraySerializer.new(obj, serializer_opts).as_json
    else
      serializer.new(obj, serializer_opts).as_json
    end
  end

  # This is odd, but it seems that in Rails `render json: obj` is about
  # 20% slower than calling MultiJSON.dump ourselves. I'm not sure why
  # Rails doesn't call MultiJson.dump when you pass it json: obj but
  # it seems we don't need whatever Rails is doing.
  def render_serialized(obj, serializer, opts={})
    render_json_dump(serialize_data(obj, serializer, opts))
  end

  def render_json_dump(obj)
    render json: MultiJson.dump(obj)
  end

  def can_cache_content?
    !current_user.present?
  end

  # Our custom cache method
  def discourse_expires_in(time_length)
    return unless can_cache_content?
    Middleware::AnonymousCache.anon_cache(request.env, time_length)
  end

  def fetch_user_from_params
    username_lower = params[:username].downcase
    username_lower.gsub!(/\.json$/, '')

    user = User.where(username_lower: username_lower).first
    raise Discourse::NotFound.new if user.blank?

    guardian.ensure_can_see!(user)
    user
  end

  def post_ids_including_replies
    post_ids = params[:post_ids].map {|p| p.to_i}
    if params[:reply_post_ids]
      post_ids << PostReply.where(post_id: params[:reply_post_ids].map {|p| p.to_i}).pluck(:reply_id)
      post_ids.flatten!
      post_ids.uniq!
    end
    post_ids
  end

  private

    def preload_anonymous_data
      store_preloaded("site", Site.cached_json(guardian))
      store_preloaded("siteSettings", SiteSetting.client_settings_json)
      store_preloaded("customHTML", custom_html_json)
    end

    def preload_current_user_data
      store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
      serializer = ActiveModel::ArraySerializer.new(TopicTrackingState.report([current_user.id]), each_serializer: TopicTrackingStateSerializer)
      store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
    end

    def custom_html_json
      MultiJson.dump({
        top: SiteContent.content_for(:top),
        bottom: SiteContent.content_for(:bottom)
      }.merge(
        (SiteSetting.tos_accept_required && !current_user) ? {tos_signup_form_message: SiteContent.content_for(:tos_signup_form_message)} : {}
      ))
    end

    def render_json_error(obj)
      if obj.present?
        render json: MultiJson.dump(errors: obj.errors.full_messages), status: 422
      else
        render json: MultiJson.dump(errors: [I18n.t('js.generic_error')]), status: 422
      end
    end

    def success_json
      {success: 'OK'}
    end

    def failed_json
      {failed: 'FAILED'}
    end

    def json_result(obj, opts={})
      if yield(obj)

        json = success_json

        # If we were given a serializer, add the class to the json that comes back
        if opts[:serializer].present?
          json[obj.class.name.underscore] = opts[:serializer].new(obj, scope: guardian).serializable_hash
        end

        render json: MultiJson.dump(json)
      else
        render_json_error(obj)
      end
    end

    def block_if_maintenance_mode
      if Discourse.maintenance_mode?
        if request.format.json?
          render status: 503, json: failed_json.merge(message: I18n.t('site_under_maintenance'))
        else
          render status: 503, file: File.join( Rails.root, 'public', '503.html' ), layout: false
        end
      end
    end

    def mini_profiler_enabled?
      defined?(Rack::MiniProfiler) && current_user.try(:admin?)
    end

    def authorize_mini_profiler
      return unless mini_profiler_enabled?
      Rack::MiniProfiler.authorize_request
    end

    def store_incoming_links
      IncomingLink.add(request,current_user) unless request.xhr?
    end

    def check_xhr
      # bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying
      return if !request.get? && api_key_valid?
      raise RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?)
    end

    def ensure_logged_in
      raise Discourse::NotLoggedIn.new unless current_user.present?
    end

    def redirect_to_login_if_required
      return if current_user || (request.format.json? && api_key_valid?)

      redirect_to :login if SiteSetting.login_required?
    end

    def build_not_found_page(status=404, layout=false)
      @top_viewed = Topic.top_viewed(10)
      @recent = Topic.recent(10)
      @slug =  params[:slug].class == String ? params[:slug] : ''
      @slug =  (params[:id].class == String ? params[:id] : '') if @slug.blank?
      @slug.gsub!('-',' ')
      render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
    end

  protected

    def api_key_valid?
      request["api_key"] && ApiKey.where(key: request["api_key"]).exists?
    end

    # returns an array of integers given a param key
    # returns nil if key is not found
    def param_to_integer_list(key, delimiter = ',')
      if params[key]
        params[key].split(delimiter).map(&:to_i)
      end
    end

end