# frozen_string_literal: true

module LiteCable
  module Connection
    # A Connection object represents a client "connected" to the application.
    # It contains all of the channel subscriptions. Incoming messages are then routed to these channel subscriptions
    # based on an identifier sent by the consumer.
    # The Connection itself does not deal with any specific application logic beyond authentication and authorization.
    #
    # Here's a basic example:
    #
    #   module MyApplication
    #     class Connection < LiteCable::Connection::Base
    #       identified_by :current_user
    #
    #       def connect
    #         self.current_user = find_verified_user
    #       end
    #
    #       def disconnect
    #         # Any cleanup work needed when the cable connection is cut.
    #       end
    #
    #       private
    #         def find_verified_user
    #           User.find_by_identity(cookies[:identity]) ||
    #             reject_unauthorized_connection
    #         end
    #     end
    #   end
    #
    # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
    # established for that current_user (and potentially disconnect them). You can declare as many
    # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
    #
    # Second, we rely on the fact that the connection is established with the cookies from the domain being sent along. This makes
    # it easy to use cookies that were set when logging in via a web interface to authorize the connection.
    #
    class Base
      include Authorization
      prepend Identification
      include Logging

      attr_reader :subscriptions, :streams, :coder

      def initialize(socket, coder: nil)
        @socket = socket
        @coder = coder || LiteCable.config.coder

        @subscriptions = Subscriptions.new(self)
        @streams = Streams.new(socket)
      end

      def handle_open
        connect if respond_to?(:connect)
        send_welcome_message
        log(:debug) { log_fmt("Opened") }
      rescue UnauthorizedError
        log(:debug) { log_fmt("Authorization failed") }
        close
      end

      def handle_close
        disconnected!
        subscriptions.remove_all

        disconnect if respond_to?(:disconnect)
        log(:debug) { log_fmt("Closed") }
      end

      def handle_command(websocket_message)
        command = decode(websocket_message)
        subscriptions.execute_command command
      rescue Subscriptions::Error, Channel::Error, Channel::Registry::Error => e
        log(:error, log_fmt("Connection command failed: #{e}"))
        close
      end

      def transmit(cable_message)
        return if disconnected?

        socket.transmit encode(cable_message)
      end

      def close
        socket.close
      end

      # Rack::Request instance of underlying socket
      def request
        socket.request
      end

      # Request cookies
      def cookies
        request.cookies
      end

      def disconnected?
        @_disconnected == true
      end

      private

      attr_reader :socket

      def disconnected!
        @_disconnected = true
      end

      def send_welcome_message
        # Send welcome message to the internal connection monitor channel.
        # This ensures the connection monitor state is reset after a successful
        # websocket connection.
        transmit type: LiteCable::INTERNAL[:message_types][:welcome]
      end

      def encode(cable_message)
        coder.encode cable_message
      end

      def decode(websocket_message)
        coder.decode websocket_message
      end

      def log_fmt(msg)
        "[connection:#{identifier}] #{msg}"
      end
    end
  end
end
