(module news-db
    (;; parameters
     connection-spec

     ;; functions
     with-db with-db/transaction
     get-sources refresh-article-view! refresh-tags-and-sources-view!
     add-user! get-user-id-by-email
     get-password-and-salt get-viewed-articles get-upvoted-articles
     article-upvoted? set-article-for-user-upvoted!
     session-delete! session-exists?
     upsert-article! get-article-cover-path get-all-images-path-and-id
     article-exists? get-article-by-id get-articles-by-tags
     get-articles-by-popularity
     get-article-points mark-article-viewed! increment-article-viewed!
     upvote-article! get-user-subscriptions
     user-tag-subscribe! user-tag-unsubscribe!
     user-author-subscribe! user-author-unsubscribe!
     get-article-id-by-guid
     get-article-author-by-source-id insert-new-author! insert-author!
     get-author-name-by-author-id get-articles-by-authors
     get-authors get-articles-by-subscriptions
     set-rank-by-tag-association set-rank-by-title set-rank-by-partial-uri
     set-rank-by-uri set-rank-by-date-difference
     set-rank-by-rank db-connection get-sources get-user-sources
     set-user-sources! add-source! get-user-settings set-user-setting!
     get-articles-viewed-by-user set-session-user-id!
     get-session-user-id user-signed-in? create-account! account-exists?
     user-has-subscriptions user-tags
     account-exists-condition account-exists-condition-predicate
     get-users-articles get-user-headers get-possible-user-sources-for-tag
     get-user-sources-for-tag set-rank-by-guids get-tags-by-rank
     add-to-breaking-news-email add-to-get-invited get-tag-groups
     set-tags-by-text set-user-default-interests!
     add-all-sources-for-tag-for-user! remove-tag-from-user
     get-tags-and-sources get-tags-by-tag-group
     add-user-source! remove-user-source! get-possible-user-sources
     set-new-user-default-interests!)

(import chicken scheme data-structures ports)

(use postgresql sql-null srfi-19 srfi-1 matchable srfi-13)

;; (define (debug a . r)
;;   (log-to (debug-log) (with-output-to-string (lambda () (apply print a
;;   r)))))

(define account-exists-condition (make-property-condition 'account-exists))
(define account-exists-condition-predicate (condition-predicate 'account-exists))

;; Update postgres parsers to handle dates.
(default-type-unparsers
  (append (default-type-unparsers)
          `((,date? . ,(lambda (conn d) (date->string d "~Y-~m-~d ~H:~M:~S"))))))

(default-type-parsers
  (append (default-type-parsers)
          `(("timestamptz" . ,(lambda (d) (string->date d "~Y-~m-~d ~H:~M:~S"))))))


(define connection-spec (make-parameter '((dbname . "news"))))
(define db-connection (make-parameter #f))

(define (with-db proc)
  (if (db-connection)
      (begin (db-connection)
             (proc (db-connection)))
      (let ((conn #f))
        (dynamic-wind
          (lambda ()
            (set! conn (connect (connection-spec))))
          (lambda () (proc conn))
          (lambda ()
            (when (and (connection? conn) (connected? conn))
              (disconnect conn)))))))

(define (with-db/transaction proc)
  (with-db
   (lambda (conn)
     (with-transaction conn
       (lambda () (proc conn))))))

(define (get-sources conn)
  (row-map* (lambda (id name icon-path)
              `((source-id . ,id) (name . ,name) (icon-path . ,icon-path)))
            (query conn "select source_id, name, icon_path from article_source;")))

(define (get-user-sources conn user-id)
  (row-map* (lambda (id name icon-path)
              `((source-id . ,id) (name . ,name) (icon-path . ,icon-path)))
            (query conn "select user_sources.source_id, name, icon_path
                         from user_sources
                         join article_source on article_source.source_id =
                         user_sources.source_id
                         where user_id=$1;" user-id)))

(define (get-user-sources-for-tag conn user-id tag)
  (row-map* (lambda (id name icon-path)
              `((source-id . ,id) (name . ,name) (icon-path . ,icon-path)))
            (query conn
                   "select article_source.source_id, article_source.name,
                    article_source.icon_path from tag_subscriptions
                    join tags on tags.tag_id = tag_subscriptions.tag_id
                    join article_source on
                        article_source.source_id = tag_subscriptions.source_id
                    where user_id=$1 and tags.name = $2;"
                   user-id tag)))

(define (get-possible-user-sources-for-tag conn user-id tag)
  (row-map* (lambda (id name icon-path)
              `((source-id . ,id) (name . ,name) (icon-path . ,icon-path)))
            (query conn
                   "select t1.source_id, name, icon_path from
                    (select distinct user_sources.source_id from user_sources
                    join article_view
                      on article_view.source_id = user_sources.source_id
                    where user_id=$1 and $2 = any(tags)) as t1
                    join article_source on article_source.source_id = t1.source_id;"
                   user-id tag)))

(define (get-possible-user-sources conn user-id)
  (row-map* (lambda (id name icon-path)
              `((source-id . ,id) (name . ,name) (icon-path . ,icon-path)))
            (query conn
                   "select distinct tags_and_sources.source_id,
                           source_name, source_icon_path from tag_subscriptions
                    join tags_and_sources
                    on tags_and_sources.tag_id = tag_subscriptions.tag_id
                    where user_id=$1;"
                   user-id)))

(define (get-tags-and-sources conn)
  (row-map*
   (lambda (source-id source-name source-icon-path tag-id tag-name tag-rank
                 groups-ids groups-names)
     `((source-id . ,source-id) (source-name . ,source-name)
       (source-icon-path . ,source-icon-path) (tag-id . ,tag-id)
       (tag-name . ,tag-name) (tag-rank . ,tag-rank)
       (groups-ids . ,(vector->list groups-ids))
       (groups-names . ,(vector->list groups-names))))
   (query
    conn
    "select source_id, source_name, source_icon_path, tag_id, tag_name, tag_rank,
     tag_groups_id::int[], tag_groups_name::text[]
     from tags_and_sources where tag_rank < 100;")))

(define (get-tags-by-tag-group conn tag-group #!key exclude-user-id)
  (row-map*
   (lambda (tag-id tag-name)
     `((tag-id . ,tag-id) (tag-name . ,tag-name)))
   (if exclude-user-id
       (query
        conn
        "select tags.tag_id, tags.name from
        (select tag_id from tags_groups
        join tag_group_names on tag_group_names.group_id = tags_groups.group_id
        where group_name=$1
        except select tag_subscriptions.tag_id from tag_subscriptions
        where user_id=$2) as t join tags on tags.tag_id = t.tag_id;"
        tag-group exclude-user-id)
       (query
        conn
        "select tags.tag_id, tags.name from
        (select tag_id from tags_groups
        join tag_group_names on tag_group_names.group_id = tags_groups.group_id
        where group_name=$1) as t join tags on tags.tag_id = t.tag_id;"
        tag-group))))

(define (set-user-sources! conn user-id sources)
  ;; TODO should do all in one query?
  (with-transaction conn
    (lambda ()
      (query conn "delete from user_sources where user_id=$1;" user-id)
      ;; TODO should do all in one query
      (for-each
       (lambda (src)
         (query conn "insert into user_sources(user_id, source_id) values ($1, $2);"
                user-id (alist-ref 'source-id src)))
       sources))))

(define (remove-tag-from-user conn user-id tag)
  (query conn "delete from tag_subscriptions
               where user_id=$1 and tag_id=(select tag_id from tags where name=$2);"
         user-id tag))

(define (add-all-sources-for-tag-for-user! conn user-id tag)
  (let ((sources
         (column-values
          (query
           conn
           "select distinct source_id from article_view where $1 = any(tags);" tag))))
    (for-each
     (lambda (src)
       (query conn "insert into user_sources(user_id, source_id) values ($1, $2);"
              user-id src))
     sources)))

(define (add-source! conn name icon-path)
  (query conn "insert into article_source(name, icon_path) values ($1, $2);"
         name icon-path))

(define (add-user-source! conn source-id user-id)
  (let ((tag-ids
         (query
          conn
          "select tags_and_sources.tag_id from tags_and_sources
           where tag_id = any((select distinct tag_id from tag_subscriptions
           where user_id=$1)) and source_id=$2;"
          user-id source-id)))
    (row-for-each*
     (lambda (tag-id)
       (query conn
              "insert into tag_subscriptions(source_id, user_id, tag_id)
               values($1, $2, $3);"
              source-id user-id tag-id))
     tag-ids)
    (query conn "insert into user_sources(user_id, source_id)
                 values ($1, $2);"
           user-id source-id)))

(define (remove-user-source! conn source-id user-id)
  (query conn "delete from tag_subscriptions where source_id=$1 and user_id=$2;"
         source-id user-id)
  (query conn "delete from user_sources where source_id=$1 and user_id=$2;;"
         source-id user-id))

(define (set-new-user-default-interests! conn user-id default-user-id)
  (row-for-each*
   (lambda (tag-id source-id)
     (query
      conn
      "insert into tag_subscriptions (user_id, tag_id, source_id) values ($1, $2, $3);"
      user-id tag-id source-id))
   (query conn "select tag_id, source_id from tag_subscriptions where user_id=$1"
          default-user-id)))

(define (set-user-default-interests! conn user-id group-name)
  (let ((tags (column-values
               (query
                conn
                "select tags.name from tags_groups
                 join tag_group_names on tag_group_names.group_id = tags_groups.group_id
                 join tags on tags.tag_id = tags_groups.tag_id
                 where group_name=$1;" group-name))))
    (for-each
     (lambda (tag)
       (let ((possible-sources (get-possible-user-sources-for-tag conn user-id tag)))
         (for-each
          (lambda (source-id)
            (query
             conn
             "insert into tag_subscriptions (user_id, tag_id, source_id)
              values ($1, (select tag_id from tags where name=$2), $3);"
             user-id tag source-id))
          (map (lambda (s) (alist-ref 'source-id s)) possible-sources))))
     tags)))

(define (get-articles-by-subscriptions conn source-id/tag-lists
                                       #!key (limit 20) rank
                                       (excludes '()))
  (query* conn (conc "select article_id, title, author, date, description,
                     rank, content, original_path, original_height, original_width,
                     tags::text[], points,
                     uri, source_name, source_icon_path, source_id, authors::text[],
                     author_ids::int[]
                     from article_view where article_id <> all($1::int[]) and "
                    (string-intersperse
                     (map-in-order
                      (lambda (i)
                        (conc " source_id = $" i " and $" (+ i 1)
                              "::varchar[] && tags "))
                      (iota (length source-id/tag-lists) 2 2))
                     " or ")
                    " order by " (if rank "rank, " "")
                    "date desc limit " limit ";")
          (append
           (list (list->vector (or excludes '())))
           (flatten
            (map-in-order
             (lambda (source-id/tag-list)
               `(,(car source-id/tag-list) ,(list->vector (cadr source-id/tag-list))))
             source-id/tag-lists)))))

;; postgres result processing.
(define (single-value/default pg-result . default)
  (if (and (> (row-count pg-result) 0) (> (column-count pg-result) 0))
      (value-at pg-result 0 0)
      (optional default (error "no result"))))

(define (refresh-article-view! conn)
  (query conn "refresh materialized view concurrently article_view;"))

(define (refresh-tags-and-sources-view! conn)
  (query conn "refresh materialized view concurrently tags_and_sources;"))

(define (add-user! conn email #!key hash salt)
  (let ((user-id (query conn "insert into user_account(password, salt, email)
                              values($1, $2, $3) returning user_id;"
                        (or hash "")
                        (or salt "")
                        email)))
    (query conn "insert into user_settings(user_id) values($1);"
           (car (row-values user-id)))
    user-id))

(define (get-user-id-by-email conn email)
  (and-let* ((_user-id
              (query conn "select user_id from user_account where email=$1;" email))
             (user-id (and (> (row-count _user-id) 0) (value-at _user-id 0 0))))
    user-id))

(define (get-password-and-salt conn email)
  (query conn "select password, salt from user_account where email=$1;" email))

(define (get-viewed-articles conn user-id)
  (column-values
   (query conn "select article_id from article_viewed_by_user
                where user_id=$1 and viewed=TRUE;"
          user-id)))

(define (get-upvoted-articles conn user-id)
  (column-values (query conn "select article_id from upvoted_by_user where user_id=$1;"
                        user-id)))

(define (article-upvoted? conn user-id article-id)
  (query conn "select article_id from upvoted_by_user
                       where user_id=$1 and article_id=$2;"
         user-id article-id))

(define (set-article-for-user-upvoted! conn user-id article-id)
  (query conn "insert into upvoted_by_user (article_id, user_id) values ($1, $2);"
         article-id user-id))

(define (session-delete! conn session-id)
  (query conn "delete from session where session_id=$1;" session-id))

(define (insert-new-tag conn tag)
  (value-at (query conn "insert into tags(name) values($1) returning tag_id;" tag) 0 0))

(define (get-article-id-by-guid conn guid-hash)
  (query conn "select article_id from articles where guid=$1;" guid-hash))

(define (get-article-author-by-source-id conn source-id)
  (query conn
         "select article_id, author from articles where source_id=$1;" source-id))

(define (get-author-name-by-author-id conn author-id)
  (query conn
         "select name from authors where author_id=$1;" author-id))

(define (get-articles-by-authors conn authors #!key limit (exclude-ids '()))
  (query conn
         "select article_id, title, author, date, description,
                 rank, content, cover_image_path, cover_image_height,
                 cover_image_width, tags::text[], points,
                 uri, source_name, source_icon_path, source_id, authors::text[],
                 author_ids::int[]
                 from get_articles_by_authors($1::varchar[], $2, $3::int[]);"
         (list->vector authors) (or limit (sql-null))
         (or (and exclude-ids (list->vector exclude-ids)) #())))

(define (get-authors conn prefix)
  (query conn
         "select author_id, name from authors where name ilike $1;"
         (conc prefix "%")))

(define (insert-new-author! conn name)
  (query conn "insert into authors(name) values ($1);" name))

(define (insert-author! conn article-id author #!key (insert-if-not-exists #t))
  (when insert-if-not-exists
    (when (=
           (row-count
            (query conn
                   "select name from authors where name=$1;" author))
           0)
      (insert-new-author! conn author)))
  (when (= (row-count
            (query conn "select 1 from article_author
                         join authors on authors.author_id = article_author.author_id
                         where article_author.article_id=$1 and name=$2;"
                   article-id author))
           0)
    (query conn "insert into article_author(article_id, author_id)
                 values($1, (select author_id from authors where name=$2));"
           article-id author)))

(define (upsert-article! conn #!key title author
                         date (tags '()) content (images '())
                         (viewed 0) (description "") (insert-tag-if-not-exists #f)
                         (insert-author-if-not-exists #t)
                         (rank 5) (article-id #f)
                         authors
                         source guid-hash uri)
  (with-transaction conn
    (lambda ()
      (let* ((new-article (not article-id))
             (article-id
              (if article-id
                  (begin
                    (query conn "update articles set title=$1, author=$2, content=$3,
                                description=$4, rank=$5, uri=$6
                                where article_id=$7;"
                           title author content description rank
                           uri article-id)
                    article-id)
                  (car (row-values
                        (query conn "insert into articles (title, author, content,
                                    date, description, rank, guid, uri,
                                    source_id)
                                    values($1, $2, $3, $4, $5, $6, $7, $8,
                                           (select source_id from article_source where
                                            name = $9))
                                    returning article_id;"
                               ;; don't set rank if rank is #f
                               title author content date description rank
                               guid-hash uri source))))))
        (for-each (lambda (author)
                    (insert-author! conn article-id author
                                    insert-if-not-exists: insert-author-if-not-exists))
                  authors)
        (for-each (lambda (tag)
                    (when insert-tag-if-not-exists
                      (when (=
                             (row-count
                              (query conn
                                     "select tag_id from tags where name=$1;" tag))
                             0)
                          (insert-new-tag conn tag)))
                    (when (=
                           (row-count
                            (query conn "select 1 from article_tags where
                                         article_id=$1 and tag_id=
                                         (select tag_id from tags where name=$2);"
                                   article-id tag))
                           0)
                        (query conn
                               "insert into article_tags(article_id, tag_id) values($1,
                                (select tag_id from tags where name=$2));"
                               article-id tag)))
                  tags)
        (for-each (lambda (img)
                    ;; Make sure we don't end up with two cover images.
                    (when (and (not new-article) (alist-ref 'cover img))
                        (query conn "update article_images set cover=FALSE
                                     where article_id=$1;" article-id))
                    (query conn
                           "insert into article_images(article_id,
                                original_path, original_width, original_height,
                                cover) values ($1, $2, $3, $4, $5);"
                           article-id
                           (alist-ref 'original-path img)
                           (alist-ref 'original-width img)
                           (alist-ref 'original-height img)
                           (alist-ref 'cover img)))
                  images)
        (when new-article
            (query conn "insert into article_stats (article_id) values ($1);"
                   article-id))
        article-id))))

(define (get-article-cover-path conn article-id)
  (let ((p (query conn "select original_path from article_images where article_id=$1
                        and cover=true;"
                  article-id)))
    (if (and (> (row-count p) 0) (> (column-count p) 0))
        (value-at p 0 0)
        #f)))

(define (get-all-images-path-and-id conn)
  (query conn "select original_path, image_id from article_images;"))

(define (article-exists? conn id)
  (> (row-count (query conn "select 1 from articles where article_id=$1;" id)) 0))

(define (get-article-by-id conn article-id)
  (row-values
   (query conn
          "select article_id, title, author, date, description,
                       rank, content, cover_image_path, cover_image_height,
                       cover_image_width, tags::text[], points,
                       uri, source_name, source_icon_path, source_id, authors::text[]
                       from get_article_by_id($1);"
          article-id)))

(define (cursor-get-all conn pg-result)
  (let* ((cur (value-at pg-result))
         (r (query conn (string-append "fetch all in \"" cur "\";"))))
    (query conn (string-append "close \"" cur "\";"))
    r))

(define (get-articles-by-tags conn tags #!key limit exclude-ids sources sort)
  (query conn
         (string-append "select article_id, title, author, date, description,
                rank, content, cover_image_path, cover_image_height,
                cover_image_width, tags::text[], points,
                uri, source_name, source_icon_path, source_id, authors::text[],
                author_ids::int[]
                from get_articles_by_tags" (if (eq? sort 'rank) "_and_rank" "")
                "($1::varchar[], $2, $3::int[], $4::int[]);")
         (list->vector tags) (or limit (sql-null))
         (or (and exclude-ids (list->vector exclude-ids)) #())
         (list->vector sources)))

;; TODO in news-init.sql, does the stored procedure need to order by
;; for the second query?
(define (get-articles-by-popularity conn #!key tags limit offset exclude-ids
                                    sources)
  (if (or (not tags) (null? tags))
      (query conn
             "select article_id, title, author, date, description,
               rank, content, cover_image_path, cover_image_height,
               cover_image_width, tags::text[], points, uri,
               source_name, source_icon_path, source_id, authors::text[],
               author_ids::int[]
               from get_articles_by_popularity
               ($1::int, $2::int, $3::int[], $4::int[]);"
             (or limit (sql-null)) (or offset 0)
             (if exclude-ids (list->vector exclude-ids) #())
             (list->vector sources))
      (query conn
             "select article_id, title, author, date, description,
               rank, content, cover_image_path, cover_image_height,
               cover_image_width, tags::text[], points, uri,
               source_name, source_icon_path, source_id, authors::text[],
               author_ids::int[]
               from get_articles_by_popularity
               ($1, $2, $3::int[], $4::text[], $5::int[]);"
             (or limit (sql-null)) (or offset 0)
             (if exclude-ids (list->vector exclude-ids) #())
             (if tags (list->vector tags) #())
             (list->vector sources))))

(define (get-user-settings conn user-id)
  (row-alist (query conn
                    "select unviewed_only as \"unviewed-only\", tracking
                     from user_settings where user_id=$1;"
                    user-id)))

(define (scheme-symbol->db-name symbol)
  (string-intersperse (string-split (symbol->string symbol) "-") "_"))

(define (set-user-setting! conn user-id setting value)
  (when (= (row-count
          (query conn "select 1 from user_settings where user_id=$1;" user-id))
         0)
    (query conn (conc "insert into user_settings(user_id, "
                      (scheme-symbol->db-name setting)
                      ") values ($1, $2);")
           value user-id))
  (query conn (conc "update user_settings set " (scheme-symbol->db-name setting)
                    "=$1 where user_id=$2;")
         value user-id))

(define (get-articles-viewed-by-user conn user-id)
  (column-values
   (query conn
          "select article_id from article_viewed_by_user
           where user_id=$1 and viewed=TRUE;" user-id)))

(define (get-article-points conn article-id #!key (add-to-params #t))
  (value-at
   (query
    conn "select points from article_stats where article_id=$1;" article-id)
   0 0))

(define (mark-article-viewed! conn article-id user-id)
  (handle-exceptions
      exn
    (if (string=?
         ((condition-property-accessor 'query 'error-code "nill") exn)
         "23505") ; unique violation, we don't care
        (void)
        (abort exn))
    (query conn
           "insert into article_viewed_by_user(article_id, user_id, viewed)
                       values($1, $2, TRUE);"
           article-id user-id)))

(define (increment-article-viewed! conn article-id)
  (query conn
         "update article_stats set view_count = view_count + 1
                    where article_id=$1;"
         article-id))

(define (upvote-article! conn article-id)
  (query conn
         "update article_stats set points = points + 1 where article_id=$1;"
         article-id))

(define (touch-session conn session-id expiration)
  (query conn "update session set expiration=$1 where session_id=$2;"
         expiration session-id))

(define (set-session-user-id! conn session-id user-id expiration
                              #!key (new-session #f))
  (if new-session
      (query conn
             "insert into session(user_id, session_id, expiration)
              values ($1, $2, $3);"
             user-id session-id expiration)
      (query conn
             "update session set user_id=$1 where session_id=$2;"
             user-id session-id))
  (touch-session conn session-id expiration))

(define (get-session-user-id conn session-id)
  (query conn "select expiration, user_id from session where session_id=$1;" session-id))

(define (session-exists? conn session-id)
  (> (row-count
      (query conn "select 1 from session where session_id=$1;" session-id))
     0))

(define (user-signed-in? conn user-id session-id)
  (> (row-count
      (query conn "select 1 from user_account
                   join session on session.user_id = user_account.user_id
                   where user_account.user_id=$1 and password != ''
                   and session_id=$2;"
             user-id session-id))
     0))

(define (account-exists? conn email)
  (> (row-count
      (query conn "select 1 from user_account where email=$1 and password != '';"
             email))
     0))

(define (create-account! conn user-id email password-hash salt)
  (if (not (account-exists? conn email))
      (query conn
             "update user_account set email=$1, password=$2, salt=$3 where user_id=$4;"
             email password-hash salt user-id)
      (signal account-exists-condition)))

(define (get-users-articles conn user-id limit #!key rank tag)
  (query* conn
          (string-append
           "select article_id, title, author, date, description,
          rank, content, cover_image_path, cover_image_height,
          cover_image_width, tags::text[], points, uri,
          source_name, source_icon_path, source_id, authors::text[],
          author_ids::int[] from
          get_tag_subscriptions_by_user"
           (if rank "_by_rank" "")
           "($1, $2"
           (if tag ", $3::varchar" "")
           ");")
          `(,user-id ,limit ,@(if tag `(,tag) '()))))

(define (get-user-subscriptions conn user-id)
  (let ((tag-subscriptions
         (query
          conn
          "select tags.name, tag_subscriptions.source_id,
           tag_subscriptions.tag_id, article_source.name, article_source.icon_path
           from tag_subscriptions
           join tags on tags.tag_id=tag_subscriptions.tag_id
           join article_source on article_source.source_id=tag_subscriptions.source_id
           where tag_subscriptions.user_id=$1;"
          user-id))
        (author-subscriptions
         (query
          conn
          "select authors.name, author_subscriptions.author_id
           from author_subscriptions
           join authors on authors.author_id=author_subscriptions.author_id
           where author_subscriptions.user_id=$1;"
          user-id)))
    (append
     (row-map* (lambda (name source-id tag-id source-name icon-path)
                 `((source-id . ,source-id) (source-name . ,source-name)
                   (type . tag) (value . ,name) (value-id . ,tag-id)
                   (source-icon-path . ,icon-path)))
               tag-subscriptions)
     (row-map* (lambda (name author-id)
                 `((type . author) (value . ,name) (value-id . ,author-id)))
               author-subscriptions))))

(define (get-user-headers conn user-id)
  (append
   (row-map*
    (lambda (name tag-id)
      `((value . ,name) (id . ,tag-id) (type . tag)))
    (query conn
           "select name, tags.tag_id from tag_subscriptions
            join tags on tags.tag_id = tag_subscriptions.tag_id
            where user_id=$1 group by name, tags.tag_id;"
           user-id))
   (row-map*
    (lambda (name author-id)
      `((value . ,name) (id . ,author-id) (type . author)))
    (query conn
           "select name, authors.author_id from author_subscriptions
            join authors on authors.author_id = author_subscriptions.author_id
            where user_id=$1;"
           user-id))))

(define (user-has-subscriptions conn user-id)
  (or
   (> (row-count
       (query conn
              "select 1 from tag_subscriptions
               where tag_subscriptions.user_id=$1 limit 1" user-id))
      0)
   (> (row-count
       (query conn
              "select 1 from author_subscriptions
               where author_subscriptions.user_id=$1 limit 1" user-id))
      0)))

(define (user-tags conn user-id)
  (query conn
         "select tags.name from tag_subscriptions
          join tags on tags.tag_id = tag_subscriptions.tag_id
          where user_id=$1 group by tags.name;"
         user-id))

(define (user-tag-subscribe! conn tag user-id source-id)
  (when (> (row-count
            (query conn
                   "select 1 from article_view where source_id=$1
                    and $2 = any(tags) limit 1;"
                   source-id tag))
           0)
    (query conn "insert into tag_subscriptions(user_id, tag_id, source_id)
                   values($1, (select tag_id from tags where name=$2), $3);"
           user-id tag source-id)))

(define (user-tag-unsubscribe! conn tag user-id . source-id)
  (if (optional source-id #f)
      (query conn
             "delete from tag_subscriptions where user_id=$1 and
              tag_id=(select tag_id from tags where name=$2) and
              source_id=$3;"
             user-id tag (car source-id))
      (query conn
             "delete from tag_subscriptions where user_id=$1 and
              tag_id=(select tag_id from tags where name=$2);"
             user-id tag)))

(define (user-author-subscribe! conn author-id user-id)
  (query conn "insert into author_subscriptions(user_id, author_id)
                   values($1, $2);"
         user-id author-id))

(define (user-author-unsubscribe! conn author-id user-id)
  (query conn
         "delete from author_subscriptions where user_id=$1 and
              author_id=$2;"
         user-id author-id))

(define (set-rank-by-tag-association conn source tag rank limit #!key (offset 0))
  (query conn
         "update articles set rank=$1 from
                (select articles.article_id from articles
                 join article_tags on article_tags.article_id = articles.article_id
                 join tags on tags.tag_id = article_tags.tag_id
                 join article_source on article_source.source_id = articles.source_id
                 where article_source.name = $2 and tags.name=$3
                 order by date desc limit $4 offset $5) as t
                where articles.article_id=t.article_id;"
         rank source tag limit offset))

(define (set-rank-by-title conn source-name titles rank)
  (query conn
         "update articles set rank=$1 from
          (select articles.article_id from articles
           join article_source on article_source.source_id = articles.source_id
           where articles.title = any($2)
           and article_source.name = $3) as t
          where articles.article_id=t.article_id;"
         rank (list->vector titles) source-name))

(define (set-rank-by-uri conn source-name uri rank)
  (query conn
         "update articles set rank=$1 from
          (select articles.article_id from articles
           join article_source on article_source.source_id = articles.source_id
           where articles.uri=$2
           and article_source.name = $3) as t
          where articles.article_id=t.article_id;"
         rank uri source-name))

(define (set-rank-by-partial-uri conn source-name uris rank)
  (query conn
         "update articles set rank=$1 from
          (select articles.article_id from articles
           join article_source on article_source.source_id = articles.source_id
           where articles.uri like any($2)
           and article_source.name = $3) as t
          where articles.article_id=t.article_id;"
         rank (list->vector
               (map (lambda (u) (string-append "%" u "%")) uris)) source-name))

(define (set-rank-by-rank conn source-name by-rank to-rank limit)
  (query conn
         "update articles set rank=$1 from
         (select articles.article_id from articles
         join article_source on article_source.source_id = articles.source_id
         where rank=$2 and article_source.name=$3 limit $4) as t
         where articles.article_id=t.article_id;"
         to-rank by-rank source-name limit))

(define (set-rank-by-guids conn rank guids)
  (query conn
         "update articles set rank=$1 where guid = any($2);"
         rank (list->vector guids)))

(define (set-rank-by-date-difference conn rank duration source-name limit offset)
  (query conn "update articles set rank=$1 from
               (select article_id from articles
                join article_source on article_source.source_id = articles.source_id
                where date > $2 and article_source.name=$3 order by date desc limit $4
                offset $5) as t
                where articles.article_id=t.article_id;"
         rank (date-subtract-duration (current-date) duration) source-name
         limit offset))

(define (get-tags-by-rank conn)
  (column-values
   (query conn
         "select name from tags where rank < 100 order by rank;")))

(define (get-tag-groups conn)
  (row-map*
   (lambda (tag-id group-name tag-name group-id)
     `((tag-id . ,tag-id) (group-name . ,group-name) (tag-name . ,tag-name)
       (group-id . ,group-id)))
   (query
    conn
    "select tags_groups.tag_id, group_name, tags.name, tags_groups.group_id
     from tags_groups
     join tag_group_names on tag_group_names.group_id = tags_groups.group_id
     join tags on tags.tag_id = tags_groups.tag_id order by rank;")))

(define (add-to-breaking-news-email conn user-id session-id email)
  (when (not (user-signed-in? conn user-id session-id))
    (query conn "update user_account set email=$1 where user_id=$2;"
           email user-id))
  (when (= (row-count
            (query conn "select 1 from breaking_news_email where email=$1;"
                   email))
           0)
    (query conn "insert into breaking_news_email(email) values($1);"
           email)))

(define (add-to-get-invited conn email)
  (when (= (row-count
            (query conn "select 1 from get_invited where email=$1;"
                   email))
           0)
    (query conn "insert into get_invited(email) values($1);"
           email)))

(define (set-tags-by-text conn text-list tag)
  (let* ((tag-id-result
          (query conn
                 "select tag_id from tags where name=$1;" tag))
         (tag-exists (> (row-count tag-id-result) 0))
         (tag-id (if tag-exists
                     (value-at tag-id-result 0 0)
                     (insert-new-tag conn tag))))
    (let ((ids
           (column-values
            (query*
             conn
             (conc
              "select article_id from (select distinct t2.article_id from (select article_id from articles order by date desc limit 50000) as t2
                  join articles on articles.article_id = t2.article_id
                  where "
              (string-intersperse
               (map-in-order
                (lambda (i)
                  (conc "description like $" i
                        " or description like $" (+ i 1)
                        " or title like $" (+ i 2)
                        " or title like $" (+ i 3)))
                (iota (length text-list) 2 4))
               " or ")
              ") as t
              where article_id <> all
                  (select article_id from article_tags where tag_id=$1);")
             (append
              (list tag-id)
              (flatten
               (map-in-order
                (lambda (text)
                  `(,(conc "%" text "%")
                    ,(conc "%" (string-titlecase text) "%")
                    ,(conc "%" text "%")
                    ,(conc "%" (string-titlecase text) "%")))
                text-list)))))))
      (for-each (lambda (id)
                  (query conn
                         "insert into article_tags(tag_id, article_id) values($1, $2);"
                         tag-id id))
                ids))))

)
