;; modules/sessions_db.fnl
;; Provides a simple interface for storing and retrieving client sessions with SQLite

;; -------------------------------------------------------------------------- ;;
;; Dependencies

(local hashids (require :hashids))
(local sqlite (require :sqlite))

;; -------------------------------------------------------------------------- ;;
;; Local modules

(local config (require :modules.config))

;; -------------------------------------------------------------------------- ;;
;; Constants

(local sql_trim_sessions_query (.. "
DELETE FROM sessions
WHERE started <= strftime('%s', 'now', '-" config.session_max_length_hours " hours')
OR last_active <= strftime('%s', 'now', '-" config.session_max_idle_length_hours " hours')
"))

;; -------------------------------------------------------------------------- ;;
;; Helper functions

(fn random_whole_number [num_digits]
  "Return a random whole number with `num_digits` digits."
  (math.floor (* (math.random) (^ 10 num_digits))))

(fn generate_session_id []
  "Return a random 24 character base62 session ID."
  (let [seed_length 10
        id_length 24
        encoder (hashids.new (tostring (random_whole_number seed_length)) id_length)]
    (encoder:encode (random_whole_number id_length))))

;; -------------------------------------------------------------------------- ;;
;; Initialize database

;; Tables:
;; sessions | Stores information about current (authorized) client sessions
;;   session_id - The unique base62 identifying string for the client session
;;   started - When the session was initiated (stored as Unix time)
;;   last_active - When the session last saw activity (stored as Unix time)

(local db (sqlite {:uri "db/sessions.db"
                   :sessions {:session_id {:type "text"
                                           :primary true}
                              :started {:type "number"
                                        :default (sqlite.lib.strftime "%s" "now")}
                              :last_active {:type "number"
                                            :default (sqlite.lib.strftime "%s" "now")}}}))

;; ---------------------------------- ;;
;; Database method overrides

(fn db.sessions.trim [self]
  "Trim expired sessions from the database."
  (let [trim_fn #(db:execute sql_trim_sessions_query)]
    (db:with_open trim_fn)))

(fn db.sessions.where [self where]
  "Return first match against `where`.
Returns a table containing the row data on success, otherwise returns nil."
  (self:trim)
  (self:__where where))

(fn db.sessions.insert [self rows]
  "Insert `rows` into the table.
Returns true on success, otherwise returns false."
  (self:trim)
  (self:__insert rows))

(fn db.sessions.update [self specs]
  "Update rows in the table according to `specs`.
Returns true on success, otherwise returns false."
  (self:trim)
  (self:__update specs))

(fn db.sessions.remove [self where]
  "Delete rows that match against `where`.
Returns true on success, otherwise returns false."
  (self:trim)
  (self:__remove where))

;; -------------------------------------------------------------------------- ;;
;; Module definition

(local sessions_db {})

(fn sessions_db.start_session []
  "Start a session and insert it into the database.
Returns the ID of the new session, or nil if the operation failed."
  (let [session_id (generate_session_id)]
    (if (and (not (db.sessions:where {: session_id}))
             (db.sessions:insert {: session_id}))
        session_id
        nil)))

(fn sessions_db.end_session [session_id]
  "End the session with session ID `session_id` by removing it from the database.
Returns true on success, otherwise returns false."
  (db.sessions:remove {: session_id}))

(fn sessions_db.is_session_active? [session_id]
  "Check the database for the presence of an active (non-expired) session with ID `session_id`.
Also updates the last_active column of the session to the current time.
Returns true if an active session is found, otherwise returns false."
  (let [current_time (os.time)]
    (if (and (db.sessions:where {: session_id})
             (db.sessions:update {:where {: session_id}
                                  :set {:last_active current_time}}))
        true
        false)))

;; -------------------------------------------------------------------------- ;;
;; Return module

sessions_db
