# frozen_string_literal: true

module Api
  module V1
    # API para recibir información sin autenticación
    class ProtectedController < BaseController
      # Permitir conexiones desde varios sitios, estamos chequeando más
      # adelante.
      skip_forgery_protection

      # Aplicar algunos chequeos básicos.  Deberíamos registrar de
      # alguna forma los errores pero tampoco queremos que nos usen
      # recursos.
      #
      # Devolvemos un error 428: Precondition Required dando la
      # oportunidad a les visitantes de reintentar en el caso de falsos
      # positivos.  No devolvemos contenido para que el servidor web
      # capture y muestra la página de error específica de cada sitio o
      # la genérica de Sutty.
      #
      # XXX: Ordenar en orden ascendiente según uso de recursos.
      before_action :cookie_is_valid?, unless: :performed?
      before_action :valid_authenticity_token_in_cookie?, unless: :performed?
      before_action :site_exists?, unless: :performed?
      before_action :site_is_origin?, unless: :performed?
      before_action :destination_exists?, unless: :performed?
      before_action :from_is_address?, unless: :performed?
      before_action :gave_consent?, unless: :performed?

      # Reescribir performed? para registrar lo que pasó en el camino a
      # menos que esté todo bien.
      def performed?
        if (performed = super) && response.status >= 400
          log_entry if ENV['DEBUG_FORMS'].present?
        end

        performed
      end

      private

      def site_cookie
        @site_cookie ||= cookies.encrypted[site_id]
      end

      # Comprueba que no se haya reutilizado una cookie vencida
      #
      # XXX: Si el navegador envió una cookie vencida es porque la está
      # reutilizando, probablemente de forma maliciosa?  Pero también
      # puede ser que haya tardado más de media hora en enviar el
      # formulario.
      def cookie_is_valid?
        return if (site_cookie&.dig('expires') || 0) > Time.now.to_i

        @reason = 'expired_or_invalid_cookie'
        render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
      end

      # Queremos comprobar que la cookie corresponda con la sesión.  La
      # cookie puede haber vencido, así que es uno de los chequeos más
      # simples que hacemos.
      #
      # TODO: Pensar una forma de redirigir al origen sin vaciar el
      # formulario para que le usuarie recargue la cookie.
      def valid_authenticity_token_in_cookie?
        return if valid_authenticity_token? session, site_cookie['csrf']

        @reason = 'invalid_auth_token'
        render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
      end

      # Comprueba que el sitio existe
      #
      # TODO: Responder con una zip bomb!
      def site_exists?
        return unless site.nil?

        @reason = 'site_does_not_exist'
        render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
      end

      # Comprueba que el mensaje fue enviado desde el sitio o uno
      # de los sitios permitidos.
      #
      # XXX: Este header se puede falsificar de todas formas pero al
      # menos es una trampa.
      def site_is_origin?
        return if origin? && site.urls(slash: false).any? { |u| origin.to_s.start_with? u }

        @reason = 'site_is_not_origin'
        render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
      end

      # Detecta si la dirección de contacto es válida.  Además es
      # opcional.
      def from_is_address?
        raise NotImplementedError
      end

      # No aceptar nada si no dió su consentimiento
      def gave_consent?
        raise NotImplementedError
      end

      def post?
        params[:layout].present?
      end

      def form?
        params[:form].present?
      end

      # Los campos que se envían tienen que corresponder con un
      # formulario de contacto o un layout
      def destination_exists?
        raise NotImplementedError
      end

      # Encuentra el sitio o devuelve nulo
      def site
        @site ||= Site.find_by(name: site_id)
      end

      # Genera un registro con información básica para debug, quizás no
      # quede asociado a ningún sitio.
      #
      # TODO: Chequear qué pasa con los archivos adjuntos.
      #
      # @return [TrueClass]
      def log_entry
        LogEntry.create site: site || Site.default, text: {
          reason: @reason,
          status: response.status,
          headers: request.headers.to_h.select { |k, _| /\A[A-Z]/ =~ k },
          params: params
        }
      end
    end
  end
end