;; Blog admin panel components

(defcomp ~blog-cache-panel (&key clear-url csrf)
  (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
    (div :class "flex flex-col md:flex-row gap-3 items-start"
      (form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML"
        (input :type "hidden" :name "csrf_token" :value csrf)
        (button :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" :type "submit" "Clear cache"))
      (div :id "cache-status" :class "py-2"))))

(defcomp ~blog-snippets-panel (&key list)
  (div :class "max-w-4xl mx-auto p-6"
    (div :class "mb-6 flex justify-between items-center"
      (h1 :class "text-3xl font-bold" "Snippets"))
    (div :id "snippets-list" list)))

(defcomp ~blog-snippet-visibility-select (&key patch-url hx-headers options cls)
  (select :name "visibility" :sx-patch patch-url :sx-target "#snippets-list" :sx-swap "innerHTML"
    :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
    options))

(defcomp ~blog-snippet-option (&key value selected label)
  (option :value value :selected selected label))

(defcomp ~blog-snippet-row (&key name owner badge-cls visibility extra)
  (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
    (div :class "flex-1 min-w-0"
      (div :class "font-medium truncate" name)
      (div :class "text-xs text-stone-500" owner))
    (span :class (str "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium " badge-cls) visibility)
    extra))

(defcomp ~blog-snippets-list (&key rows)
  (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows)))

(defcomp ~blog-menu-items-panel (&key new-url list)
  (div :class "max-w-4xl mx-auto p-6"
    (div :class "mb-6 flex justify-end items-center"
      (button :type "button" :sx-get new-url :sx-target "#menu-item-form" :sx-swap "innerHTML"
        :class "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
        (i :class "fa fa-plus") " Add Menu Item"))
    (div :id "menu-item-form" :class "mb-6")
    (div :id "menu-items-list" list)))

(defcomp ~blog-menu-item-row (&key img label slug sort-order edit-url delete-url confirm-text hx-headers)
  (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
    (div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical"))
    img
    (div :class "flex-1 min-w-0"
      (div :class "font-medium truncate" label)
      (div :class "text-xs text-stone-500 truncate" slug))
    (div :class "text-sm text-stone-500" (str "Order: " sort-order))
    (div :class "flex gap-2 flex-shrink-0"
      (button :type "button" :sx-get edit-url :sx-target "#menu-item-form" :sx-swap "innerHTML"
        :class "px-3 py-1 text-sm bg-stone-200 hover:bg-stone-300 rounded"
        (i :class "fa fa-edit") " Edit")
      (~delete-btn :url delete-url :trigger-target "#menu-items-list"
        :title "Delete menu item?" :text confirm-text
        :sx-headers hx-headers))))

(defcomp ~blog-menu-items-list (&key rows)
  (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows)))

;; Tag groups admin

(defcomp ~blog-tag-groups-create-form (&key create-url csrf)
  (form :method "post" :action create-url :class "border rounded p-4 bg-white space-y-3"
    (input :type "hidden" :name "csrf_token" :value csrf)
    (h3 :class "text-sm font-semibold text-stone-700" "New Group")
    (div :class "flex flex-col sm:flex-row gap-3"
      (input :type "text" :name "name" :placeholder "Group name" :required "" :class "flex-1 border rounded px-3 py-2 text-sm")
      (input :type "text" :name "colour" :placeholder "#colour" :class "w-28 border rounded px-3 py-2 text-sm")
      (input :type "number" :name "sort_order" :placeholder "Order" :value "0" :class "w-20 border rounded px-3 py-2 text-sm"))
    (input :type "text" :name "feature_image" :placeholder "Image URL (optional)" :class "w-full border rounded px-3 py-2 text-sm")
    (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Create")))

(defcomp ~blog-tag-group-icon-image (&key src name)
  (img :src src :alt name :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))

(defcomp ~blog-tag-group-icon-color (&key style initial)
  (div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0"
    :style style initial))

(defcomp ~blog-tag-group-li (&key icon edit-href name slug sort-order)
  (li :class "border rounded p-3 bg-white flex items-center gap-3"
    icon
    (div :class "flex-1"
      (a :href edit-href :class "font-medium text-stone-800 hover:underline" name)
      (span :class "text-xs text-stone-500 ml-2" slug))
    (span :class "text-xs text-stone-500" (str "order: " sort-order))))

(defcomp ~blog-tag-groups-list (&key items)
  (ul :class "space-y-2" items))

(defcomp ~blog-unassigned-tag (&key name)
  (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200 rounded" name))

(defcomp ~blog-unassigned-tags (&key heading spans)
  (div :class "border-t pt-4"
    (h3 :class "text-sm font-semibold text-stone-700 mb-2" heading)
    (div :class "flex flex-wrap gap-2" spans)))

(defcomp ~blog-tag-groups-main (&key form groups unassigned)
  (div :class "max-w-2xl mx-auto px-4 py-6 space-y-8"
    form groups unassigned))

;; Tag group edit

(defcomp ~blog-tag-checkbox (&key tag-id checked img name)
  (label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer"
    (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300")
    img (span name)))

(defcomp ~blog-tag-checkbox-image (&key src)
  (img :src src :alt "" :class "h-4 w-4 rounded-full object-cover"))

(defcomp ~blog-tag-group-edit-form (&key save-url csrf name colour sort-order feature-image tags)
  (form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4"
    (input :type "hidden" :name "csrf_token" :value csrf)
    (div :class "space-y-3"
      (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Name")
        (input :type "text" :name "name" :value name :required "" :class "w-full border rounded px-3 py-2 text-sm"))
      (div :class "flex gap-3"
        (div :class "flex-1" (label :class "block text-xs font-medium text-stone-600 mb-1" "Colour")
          (input :type "text" :name "colour" :value colour :placeholder "#hex" :class "w-full border rounded px-3 py-2 text-sm"))
        (div :class "w-24" (label :class "block text-xs font-medium text-stone-600 mb-1" "Order")
          (input :type "number" :name "sort_order" :value sort-order :class "w-full border rounded px-3 py-2 text-sm")))
      (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Feature Image URL")
        (input :type "text" :name "feature_image" :value feature-image :placeholder "https://..." :class "w-full border rounded px-3 py-2 text-sm")))
    (div (label :class "block text-xs font-medium text-stone-600 mb-2" "Assign Tags")
      (div :class "grid grid-cols-1 sm:grid-cols-2 gap-1 max-h-64 overflow-y-auto border rounded p-2"
        tags))
    (div :class "flex gap-3"
      (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save"))))

(defcomp ~blog-tag-group-delete-form (&key delete-url csrf)
  (form :method "post" :action delete-url :class "border-t pt-4"
    :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')"
    (input :type "hidden" :name "csrf_token" :value csrf)
    (button :type "submit" :class "border rounded px-4 py-2 bg-red-600 text-white text-sm" "Delete Group")))

(defcomp ~blog-tag-group-edit-main (&key edit-form delete-form)
  (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
    edit-form delete-form))

;; Preview panel components

(defcomp ~blog-preview-panel (&key sections)
  (div :class "max-w-4xl mx-auto px-4 py-6 space-y-4"
    (style "
      .sx-pretty, .json-pretty { font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
      .sx-list, .json-obj, .json-arr { display: block; }
      .sx-paren { color: #64748b; }
      .sx-sym { color: #0369a1; }
      .sx-kw { color: #7c3aed; }
      .sx-str { color: #15803d; }
      .sx-num { color: #c2410c; }
      .sx-bool { color: #b91c1c; font-weight: 600; }
      .json-brace, .json-bracket { color: #64748b; }
      .json-key { color: #7c3aed; }
      .json-str { color: #15803d; }
      .json-num { color: #c2410c; }
      .json-lit { color: #b91c1c; font-weight: 600; }
      .json-field { display: block; }
    ")
    sections))

(defcomp ~blog-preview-section (&key title content)
  (details :class "border rounded bg-white"
    (summary :class "cursor-pointer px-4 py-3 font-medium text-sm bg-stone-100 hover:bg-stone-200 select-none" title)
    (div :class "p-4 overflow-x-auto text-xs" content)))

(defcomp ~blog-preview-rendered (&key html)
  (div :class "blog-content prose max-w-none" (raw! html)))

(defcomp ~blog-preview-empty ()
  (div :class "p-8 text-stone-500" "No content to preview."))

(defcomp ~blog-admin-placeholder ()
  (div :class "pb-8"))

;; ---------------------------------------------------------------------------
;; Data-driven content defcomps (called from defpages with service data)
;; ---------------------------------------------------------------------------

;; Snippets — receives serialized snippet dicts from service
(defcomp ~blog-snippets-content (&key snippets is-admin csrf)
  (~blog-snippets-panel
    :list (if (empty? (or snippets (list)))
      (~empty-state :icon "fa fa-puzzle-piece"
        :message "No snippets yet. Create one from the blog editor.")
      (~blog-snippets-list
        :rows (map (lambda (s)
          (let* ((badge-colours (dict
                   "private" "bg-stone-200 text-stone-700"
                   "shared" "bg-blue-100 text-blue-700"
                   "admin" "bg-amber-100 text-amber-700"))
                 (vis (or (get s "visibility") "private"))
                 (badge-cls (or (get badge-colours vis) "bg-stone-200 text-stone-700"))
                 (name (get s "name"))
                 (owner (get s "owner"))
                 (can-delete (get s "can_delete")))
            (~blog-snippet-row
              :name name :owner owner :badge-cls badge-cls :visibility vis
              :extra (<>
                (when is-admin
                  (~blog-snippet-visibility-select
                    :patch-url (get s "patch_url")
                    :hx-headers {:X-CSRFToken csrf}
                    :options (<>
                      (~blog-snippet-option :value "private" :selected (= vis "private") :label "private")
                      (~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared")
                      (~blog-snippet-option :value "admin" :selected (= vis "admin") :label "admin"))))
                (when can-delete
                  (~delete-btn
                    :url (get s "delete_url")
                    :trigger-target "#snippets-list"
                    :title "Delete snippet?"
                    :text (str "Delete \u201c" name "\u201d?")
                    :sx-headers {:X-CSRFToken csrf}
                    :cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))))
          (or snippets (list)))))))

;; Menu Items — receives serialized menu item dicts from service
(defcomp ~blog-menu-items-content (&key menu-items new-url csrf)
  (~blog-menu-items-panel
    :new-url new-url
    :list (if (empty? (or menu-items (list)))
      (~empty-state :icon "fa fa-inbox"
        :message "No menu items yet. Add one to get started!")
      (~blog-menu-items-list
        :rows (map (lambda (mi)
          (~blog-menu-item-row
            :img (~img-or-placeholder
              :src (get mi "feature_image") :alt (get mi "label")
              :size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")
            :label (get mi "label")
            :slug (get mi "url")
            :sort-order (str (or (get mi "sort_order") 0))
            :edit-url (get mi "edit_url")
            :delete-url (get mi "delete_url")
            :confirm-text (str "Remove " (get mi "label") " from the menu?")
            :hx-headers {:X-CSRFToken csrf}))
          (or menu-items (list)))))))

;; Tag Groups — receives serialized tag group data from service
(defcomp ~blog-tag-groups-content (&key groups unassigned-tags create-url csrf)
  (~blog-tag-groups-main
    :form (~blog-tag-groups-create-form :create-url create-url :csrf csrf)
    :groups (if (empty? (or groups (list)))
      (~empty-state :icon "fa fa-tags" :message "No tag groups yet.")
      (~blog-tag-groups-list
        :items (map (lambda (g)
          (let* ((fi (get g "feature_image"))
                 (colour (get g "colour"))
                 (name (get g "name"))
                 (initial (slice (or name "?") 0 1))
                 (icon (if fi
                   (~blog-tag-group-icon-image :src fi :name name)
                   (~blog-tag-group-icon-color
                     :style (if colour (str "background:" colour) "background:#e7e5e4")
                     :initial initial))))
            (~blog-tag-group-li
              :icon icon
              :edit-href (get g "edit_href")
              :name name
              :slug (or (get g "slug") "")
              :sort-order (or (get g "sort_order") 0))))
          (or groups (list)))))
    :unassigned (when (not (empty? (or unassigned-tags (list))))
      (~blog-unassigned-tags
        :heading (str (len (or unassigned-tags (list))) " Unassigned Tags")
        :spans (map (lambda (t)
          (~blog-unassigned-tag :name (get t "name")))
          (or unassigned-tags (list)))))))

;; Tag Group Edit — receives serialized tag group + tags from service
(defcomp ~blog-tag-group-edit-content (&key group all-tags save-url delete-url csrf)
  (~blog-tag-group-edit-main
    :edit-form (~blog-tag-group-edit-form
      :save-url save-url :csrf csrf
      :name (get group "name")
      :colour (get group "colour")
      :sort-order (get group "sort_order")
      :feature-image (get group "feature_image")
      :tags (map (lambda (t)
        (~blog-tag-checkbox
          :tag-id (get t "id")
          :checked (get t "checked")
          :img (when (get t "feature_image")
            (~blog-tag-checkbox-image :src (get t "feature_image")))
          :name (get t "name")))
        (or all-tags (list))))
    :delete-form (~blog-tag-group-delete-form :delete-url delete-url :csrf csrf)))

;; ---------------------------------------------------------------------------
;; Preview content composition — replaces _h_post_preview_content
;; ---------------------------------------------------------------------------

(defcomp ~blog-preview-content (&key sx-pretty json-pretty sx-rendered lex-rendered)
  (let* ((sections (list)))
    (if (and (not sx-pretty) (not json-pretty) (not sx-rendered) (not lex-rendered))
      (~blog-preview-empty)
      (~blog-preview-panel :sections
        (<>
          (when sx-pretty
            (~blog-preview-section :title "S-Expression Source" :content sx-pretty))
          (when json-pretty
            (~blog-preview-section :title "Lexical JSON" :content json-pretty))
          (when sx-rendered
            (~blog-preview-section :title "SX Rendered"
              :content (~blog-preview-rendered :html sx-rendered)))
          (when lex-rendered
            (~blog-preview-section :title "Lexical Rendered"
              :content (~blog-preview-rendered :html lex-rendered))))))))

;; ---------------------------------------------------------------------------
;; Data introspection composition — replaces _h_post_data_content
;; ---------------------------------------------------------------------------

(defcomp ~blog-data-value-cell (&key value value-type)
  (if (= value-type "nil")
    (span :class "text-neutral-400" "\u2014")
    (pre :class "whitespace-pre-wrap break-words break-all text-xs"
      (if (or (= value-type "date") (= value-type "other"))
        (code value)
        value))))

(defcomp ~blog-data-scalar-table (&key columns)
  (div :class "w-full overflow-x-auto sm:overflow-visible"
    (table :class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden"
      (thead :class "bg-neutral-50/70"
        (tr (th :class "px-3 py-2 text-left font-medium w-40 sm:w-56" "Field")
            (th :class "px-3 py-2 text-left font-medium" "Value")))
      (tbody
        (map (lambda (col)
          (tr :class "border-t border-neutral-200 align-top"
            (td :class "px-3 py-2 whitespace-nowrap text-neutral-600 align-top" (get col "key"))
            (td :class "px-3 py-2 align-top"
              (~blog-data-value-cell :value (get col "value") :value-type (get col "type")))))
          (or columns (list)))))))

(defcomp ~blog-data-relationship-item (&key index summary children)
  (tr :class "border-t border-neutral-200 align-top"
    (td :class "px-2 py-1 whitespace-nowrap align-top" (str index))
    (td :class "px-2 py-1 align-top"
      (pre :class "whitespace-pre-wrap break-words break-all text-xs"
        (code summary))
      (when children
        (div :class "mt-2 pl-3 border-l border-neutral-200"
          (~blog-data-model-content
            :columns (get children "columns")
            :relationships (get children "relationships")))))))

(defcomp ~blog-data-relationship (&key name cardinality class-name loaded value)
  (div :class "rounded-xl border border-neutral-200"
    (div :class "px-3 py-2 bg-neutral-50/70 text-sm font-medium"
      "Relationship: " (span :class "font-semibold" name)
      (span :class "ml-2 text-xs text-neutral-500"
        cardinality " \u2192 " class-name
        (when (not loaded) " \u2022 " (em "not loaded"))))
    (div :class "p-3 text-sm"
      (if (not value)
        (span :class "text-neutral-400" "\u2014")
        (if (get value "is_list")
          (<>
            (div :class "text-neutral-500 mb-2"
              (str (get value "count") " item" (if (= (get value "count") 1) "" "s")))
            (when (get value "items")
              (div :class "w-full overflow-x-auto sm:overflow-visible"
                (table :class "w-full table-fixed text-sm border border-neutral-200 rounded-lg overflow-hidden"
                  (thead :class "bg-neutral-50/70"
                    (tr (th :class "px-2 py-1 text-left w-10" "#")
                        (th :class "px-2 py-1 text-left" "Summary")))
                  (tbody
                    (map (lambda (item)
                      (~blog-data-relationship-item
                        :index (get item "index")
                        :summary (get item "summary")
                        :children (get item "children")))
                      (get value "items")))))))
          ;; Single value
          (<>
            (pre :class "whitespace-pre-wrap break-words break-all text-xs mb-2"
              (code (get value "summary")))
            (when (get value "children")
              (div :class "pl-3 border-l border-neutral-200"
                (~blog-data-model-content
                  :columns (get (get value "children") "columns")
                  :relationships (get (get value "children") "relationships"))))))))))

(defcomp ~blog-data-model-content (&key columns relationships)
  (div :class "space-y-4"
    (~blog-data-scalar-table :columns columns)
    (when (not (empty? (or relationships (list))))
      (div :class "space-y-3"
        (map (lambda (rel)
          (~blog-data-relationship
            :name (get rel "name")
            :cardinality (get rel "cardinality")
            :class-name (get rel "class_name")
            :loaded (get rel "loaded")
            :value (get rel "value")))
          relationships)))))

(defcomp ~blog-data-table-content (&key tablename model-data)
  (if (not model-data)
    (div :class "px-4 py-8 text-stone-400" "No post data available.")
    (div :class "px-4 py-8"
      (div :class "mb-6 text-sm text-neutral-500"
        "Model: " (code "Post") " \u2022 Table: " (code tablename))
      (~blog-data-model-content
        :columns (get model-data "columns")
        :relationships (get model-data "relationships")))))
