;; ==========================================================================
;; sx-pub API endpoints — actor, webfinger, collections, publishing, browsing
;;
;; All responses are text/sx. No JSON.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Actor
;; --------------------------------------------------------------------------

(defhandler
  pub-actor
  :path "/pub/actor"
  :method :get
  :returns "element"
  (&key)
  (let-match
    {:domain domain :summary summary :display-name display-name :public-key-pem public-key-pem}
    (helper "pub-actor-data")
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (str
        "(SxActor"
        "\n  :id \"https://"
        domain
        "/pub/actor\""
        "\n  :type \"SxPublisher\""
        "\n  :name \""
        display-name
        "\""
        "\n  :summary \""
        summary
        "\""
        "\n  :inbox \"/pub/inbox\""
        "\n  :outbox \"/pub/outbox\""
        "\n  :followers \"/pub/followers\""
        "\n  :following \"/pub/following\""
        "\n  :public-key-pem \""
        public-key-pem
        "\")"))))


;; --------------------------------------------------------------------------
;; Webfinger
;; --------------------------------------------------------------------------

(defhandler
  pub-webfinger
  :path "/pub/webfinger"
  :method :get
  :returns "element"
  (&key)
  (let
    ((resource (helper "request-arg" "resource" "")))
    (let-match
      {:domain domain :preferred-username preferred-username}
      (helper "pub-actor-data")
      (let
        ((expected (str "acct:" preferred-username "@" domain)))
        (if
          (!= resource expected)
          (do
            (set-response-status 404)
            (str "(Error :message \"Resource not found\")"))
          (do
            (set-response-header "Content-Type" "text/sx; charset=utf-8")
            (str
              "(SxWebfinger"
              "\n  :subject \""
              expected
              "\""
              "\n  :actor \"https://"
              domain
              "/pub/actor\""
              "\n  :type \"SxPublisher\")")))))))


;; --------------------------------------------------------------------------
;; Collections
;; --------------------------------------------------------------------------

(defhandler
  pub-collections
  :path "/pub/collections"
  :method :get
  :returns "element"
  (&key)
  (let
    ((collections (helper "pub-collections-data")))
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (let
        ((items (map (fn (c) (let-match {:description description :slug slug :name name} c (str "\n  (SxCollection" " :slug \"" slug "\"" " :name \"" name "\"" " :description \"" description "\"" " :href \"/pub/" slug "\")"))) collections)))
        (str "(SxCollections" (join "" items) ")")))))


;; --------------------------------------------------------------------------
;; Status
;; --------------------------------------------------------------------------

(defhandler
  pub-status
  :path "/pub/status"
  :method :get
  :returns "element"
  (&key)
  (let-match
    {:db db :domain domain :healthy healthy :ipfs ipfs :actor actor}
    (helper "pub-status-data")
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (str
        "(SxPubStatus"
        "\n  :healthy "
        healthy
        "\n  :db \""
        db
        "\""
        "\n  :ipfs \""
        ipfs
        "\""
        "\n  :actor \""
        actor
        "\""
        "\n  :domain \""
        (or domain "unknown")
        "\")"))))


;; ==========================================================================
;; Phase 2: Publishing + Browsing
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Publish
;; --------------------------------------------------------------------------

(defhandler
  pub-publish
  :path "/pub/publish"
  :method :post
  :csrf false
  :returns "element"
  (&key)
  (let
    ((collection (helper "request-form" "collection" ""))
      (slug (helper "request-form" "slug" ""))
      (content (helper "request-form" "content" ""))
      (title (helper "request-form" "title" ""))
      (summary (helper "request-form" "summary" "")))
    (if
      (or (= collection "") (= slug "") (= content ""))
      (do
        (set-response-status 400)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        "(Error :message \"Missing collection, slug, or content\")")
      (let
        ((result (helper "pub-publish" collection slug content title summary)))
        (if
          (get result "error")
          (do
            (set-response-status 500)
            (set-response-header "Content-Type" "text/sx; charset=utf-8")
            (str "(Error :message \"" (get result "error") "\")"))
          (let-match
            {:cid cid :hash hash :size size :title title :path path :slug slug :collection collection}
            result
            (do
              (set-response-header "Content-Type" "text/sx; charset=utf-8")
              (str
                "(Published"
                "\n  :path \""
                path
                "\""
                "\n  :cid \""
                cid
                "\""
                "\n  :hash \""
                hash
                "\""
                "\n  :size "
                size
                "\n  :collection \""
                collection
                "\""
                "\n  :slug \""
                slug
                "\""
                "\n  :title \""
                title
                "\")"))))))))


;; --------------------------------------------------------------------------
;; Browse collection
;; --------------------------------------------------------------------------

(defhandler
  pub-browse-collection
  :path "/pub/browse/<collection_slug>"
  :method :get
  :returns "element"
  (&key collection_slug)
  (let
    ((data (helper "pub-collection-items" collection_slug)))
    (if
      (get data "error")
      (do
        (set-response-status 404)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        (str "(Error :message \"" (get data "error") "\")"))
      (let-match
        {:description description :items items-data :collection collection :name name}
        data
        (do
          (set-response-header "Content-Type" "text/sx; charset=utf-8")
          (let
            ((items (map (fn (d) (let-match {:cid cid :size size :summary summary :title title :slug slug} d (str "\n  (SxDocument" " :slug \"" slug "\"" " :title \"" title "\"" " :summary \"" summary "\"" " :cid \"" cid "\"" " :size " size ")"))) items-data)))
            (str
              "(SxCollection"
              "\n  :slug \""
              collection
              "\""
              "\n  :name \""
              name
              "\""
              "\n  :description \""
              description
              "\""
              (join "" items)
              ")")))))))


;; --------------------------------------------------------------------------
;; Resolve document by path
;; --------------------------------------------------------------------------

(defhandler pub-document
  :path "/pub/doc/<collection_slug>/<slug>"
  :method :get
  :returns "element"
  (&key collection_slug slug)
  (let ((data (helper "pub-resolve-document" collection_slug slug)))
    (if (get data "error")
      (do
        (set-response-status 404)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        (str "(Error :message \"" (get data "error") "\")"))
      (do
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        (set-response-header "X-IPFS-CID" (get data "cid"))
        (get data "content")))))


;; --------------------------------------------------------------------------
;; Direct CID fetch
;; --------------------------------------------------------------------------

(defhandler pub-cid
  :path "/pub/cid/<cid>"
  :method :get
  :returns "element"
  (&key cid)
  (let ((data (helper "pub-fetch-cid" cid)))
    (if (get data "error")
      (do
        (set-response-status 404)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        (str "(Error :message \"" (get data "error") "\")"))
      (do
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        (set-response-header "Cache-Control" "public, max-age=31536000, immutable")
        (get data "content")))))


;; ==========================================================================
;; Phase 3: Federation — outbox, inbox, follow, followers, following
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Outbox
;; --------------------------------------------------------------------------

(defhandler
  pub-outbox
  :path "/pub/outbox"
  :method :get
  :returns "element"
  (&key)
  (let
    ((page (helper "request-arg" "page" ""))
      (data (helper "pub-outbox-data" page)))
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (let-match
        {:total total :page page :items items-data}
        data
        (let
          ((items (map (fn (a) (let-match {:cid cid :type type :object-type object-type :published published} a (str "\n  (" type " :object-type \"" object-type "\"" " :published \"" published "\"" " :cid \"" cid "\")"))) items-data)))
          (str
            "(SxOutbox"
            "\n  :total "
            total
            "\n  :page "
            page
            (join "" items)
            ")"))))))


;; --------------------------------------------------------------------------
;; Inbox
;; --------------------------------------------------------------------------

(defhandler pub-inbox
  :path "/pub/inbox"
  :method :post
  :csrf false
  :returns "element"
  (&key)
  (let ((body (helper "pub-request-body")))
    (if (= body "")
      (do
        (set-response-status 400)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        "(Error :message \"Empty body\")")
      (let ((result (helper "pub-process-inbox" body)))
        (do
          (set-response-status 202)
          (set-response-header "Content-Type" "text/sx; charset=utf-8")
          (str "(Accepted :result " (str result) ")"))))))


;; --------------------------------------------------------------------------
;; Follow a remote server
;; --------------------------------------------------------------------------

(defhandler
  pub-follow
  :path "/pub/follow"
  :method :post
  :csrf false
  :returns "element"
  (&key)
  (let
    ((actor-url (helper "request-form" "actor_url" "")))
    (if
      (= actor-url "")
      (do
        (set-response-status 400)
        (set-response-header "Content-Type" "text/sx; charset=utf-8")
        "(Error :message \"Missing actor_url\")")
      (let
        ((result (helper "pub-follow-remote" actor-url)))
        (do
          (set-response-header "Content-Type" "text/sx; charset=utf-8")
          (if
            (get result "error")
            (do
              (set-response-status 502)
              (str "(Error :message \"" (get result "error") "\")"))
            (let-match
              {:status status :actor-url actor-url}
              result
              (str
                "(FollowSent"
                "\n  :actor-url \""
                actor-url
                "\""
                "\n  :status \""
                status
                "\")"))))))))


;; --------------------------------------------------------------------------
;; Followers
;; --------------------------------------------------------------------------

(defhandler
  pub-followers
  :path "/pub/followers"
  :method :get
  :returns "element"
  (&key)
  (let
    ((data (helper "pub-followers-data")))
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (let
        ((items (map (fn (f) (let-match {:actor-url actor-url :acct acct} f (str "\n  (SxFollower" " :acct \"" acct "\"" " :actor-url \"" actor-url "\")"))) data)))
        (str "(SxFollowers" (join "" items) ")")))))


;; --------------------------------------------------------------------------
;; Following
;; --------------------------------------------------------------------------

(defhandler pub-following
  :path "/pub/following"
  :method :get
  :returns "element"
  (&key)
  (let ((data (helper "pub-following-data")))
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (let ((items (map (fn (f)
              (str "\n  (SxFollowing"
                " :actor-url \"" (get f "actor-url") "\")"))
              data)))
        (str "(SxFollowing" (join "" items) ")")))))


;; ==========================================================================
;; Phase 4: Anchoring — Merkle trees, OTS, verification
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Anchor pending activities
;; --------------------------------------------------------------------------

(defhandler
  pub-anchor
  :path "/pub/anchor"
  :method :post
  :csrf false
  :returns "element"
  (&key)
  (let-match
    {:tree-cid tree-cid :status status :count count :ots-proof-cid ots-proof-cid :merkle-root merkle-root}
    (helper "pub-anchor-pending")
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (if
        (= status "nothing-to-anchor")
        "(Anchor :status \"nothing-to-anchor\" :count 0)"
        (str
          "(Anchor"
          "\n  :status \""
          status
          "\""
          "\n  :count "
          count
          "\n  :merkle-root \""
          merkle-root
          "\""
          "\n  :tree-cid \""
          tree-cid
          "\""
          "\n  :ots-proof-cid \""
          ots-proof-cid
          "\")")))))


;; --------------------------------------------------------------------------
;; Verify a CID's anchor
;; --------------------------------------------------------------------------

(defhandler
  pub-verify
  :path "/pub/verify/<cid>"
  :method :get
  :returns "element"
  (&key cid)
  (let
    ((data (helper "pub-verify-anchor" cid)))
    (do
      (set-response-header "Content-Type" "text/sx; charset=utf-8")
      (if
        (get data "error")
        (do
          (set-response-status 404)
          (str "(Error :message \"" (get data "error") "\")"))
        (let-match
          {:cid cid* :tree-cid tree-cid :status status :verified verified :ots-proof-cid ots-proof-cid :merkle-root merkle-root :published published}
          data
          (str
            "(AnchorVerification"
            "\n  :cid \""
            cid*
            "\""
            "\n  :status \""
            status
            "\""
            "\n  :verified "
            verified
            "\n  :merkle-root \""
            merkle-root
            "\""
            "\n  :tree-cid \""
            tree-cid
            "\""
            "\n  :ots-proof-cid \""
            ots-proof-cid
            "\""
            "\n  :published \""
            published
            "\")"))))))
