// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/berrypod"
import topbar from "../vendor/topbar"

// Hook to sync color picker and text input
const ColorSync = {
  mounted() {
    const picker = this.el.querySelector('input[type="color"]')
    const text = this.el.querySelector('input[type="text"]')

    if (picker && text) {
      picker.addEventListener('input', (e) => {
        text.value = e.target.value
      })

      text.addEventListener('input', (e) => {
        picker.value = e.target.value
      })
    }
  }
}

// Hook to persist cart to session via API
const CartPersist = {
  mounted() {
    this.handleEvent("persist_cart", ({items}) => {
      const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
      fetch("/api/cart", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-csrf-token": csrfToken
        },
        body: JSON.stringify({items})
      })
    })

    this.handleEvent("persist_country", ({code}) => {
      document.cookie = `shipping_country=${code};path=/;max-age=${60 * 60 * 24 * 365};SameSite=Lax`
    })
  }
}

// Hook for accessible cart drawer (WAI-ARIA dialog pattern)
const CartDrawer = {
  mounted() {
    this.triggerElement = null
    this.boundKeydown = this.handleKeydown.bind(this)
    this.boundTouchmove = this.handleTouchmove.bind(this)
    this.scrollY = 0
    this.isOpen = this.el.classList.contains('open')

    // Prevent the cart link from navigating when the drawer is available
    this._cartTrigger = document.querySelector('a[aria-label="Cart"]')
    if (this._cartTrigger) {
      this._preventNav = (e) => e.preventDefault()
      this._cartTrigger.addEventListener("click", this._preventNav)
    }

    if (this.isOpen) {
      this.lockScroll()
      document.addEventListener('keydown', this.boundKeydown)
    }
  },

  updated() {
    const nowOpen = this.el.classList.contains('open')
    if (nowOpen && !this.isOpen) {
      this.onOpen()
    } else if (!nowOpen && this.isOpen) {
      this.onClose()
    }
    this.isOpen = nowOpen
  },

  lockScroll() {
    // Store current scroll position
    this.scrollY = window.scrollY

    // Lock body scroll (works on mobile too)
    document.body.style.position = 'fixed'
    document.body.style.top = `-${this.scrollY}px`
    document.body.style.left = '0'
    document.body.style.right = '0'
    document.body.style.overflow = 'hidden'

    // Prevent touchmove on document (extra safety for iOS)
    document.addEventListener('touchmove', this.boundTouchmove, { passive: false })
  },

  unlockScroll() {
    // Restore body scroll
    document.body.style.position = ''
    document.body.style.top = ''
    document.body.style.left = ''
    document.body.style.right = ''
    document.body.style.overflow = ''

    // Restore scroll position
    window.scrollTo(0, this.scrollY)

    // Remove touchmove listener
    document.removeEventListener('touchmove', this.boundTouchmove)
  },

  handleTouchmove(e) {
    // Allow scrolling inside the drawer itself
    if (this.el.contains(e.target)) {
      return
    }
    e.preventDefault()
  },

  onOpen() {
    // Store trigger for focus return
    this.triggerElement = document.activeElement

    // Lock scroll
    this.lockScroll()

    // Focus first focusable element (close button)
    const firstFocusable = this.el.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    if (firstFocusable) {
      setTimeout(() => firstFocusable.focus(), 50)
    }

    // Enable focus trap and Escape handling
    document.addEventListener('keydown', this.boundKeydown)
  },

  onClose() {
    // Unlock scroll
    this.unlockScroll()

    // Remove keyboard listener
    document.removeEventListener('keydown', this.boundKeydown)

    // Return focus to trigger element
    if (this.triggerElement) {
      this.triggerElement.focus()
    }
  },

  handleKeydown(e) {
    // Close on Escape - let server handle the state change
    if (e.key === 'Escape') {
      this.pushEvent("close_cart_drawer")
      return
    }

    // Focus trap on Tab
    if (e.key === 'Tab') {
      const focusable = this.el.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      )
      const first = focusable[0]
      const last = focusable[focusable.length - 1]

      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first.focus()
      }
    }
  },

  destroyed() {
    document.removeEventListener('keydown', this.boundKeydown)
    document.removeEventListener('touchmove', this.boundTouchmove)
    if (this._cartTrigger && this._preventNav) {
      this._cartTrigger.removeEventListener("click", this._preventNav)
    }
    document.body.style.position = ''
    document.body.style.top = ''
    document.body.style.left = ''
    document.body.style.right = ''
    document.body.style.overflow = ''
  }
}

// Hook for image lightbox (used by product gallery and review photos)
const Lightbox = {
  mounted() {
    const dialog = this.el
    // Use class selectors so multiple lightboxes can coexist
    const lightboxImage = dialog.querySelector('.lightbox-image')
    const lightboxCounter = dialog.querySelector('.lightbox-counter')

    // Get images from data attribute
    const getImages = () => {
      try {
        return JSON.parse(dialog.dataset.images || '[]')
      } catch {
        return []
      }
    }

    const getCurrentIndex = () => parseInt(dialog.dataset.currentIndex || '0', 10)
    const setCurrentIndex = (idx) => { dialog.dataset.currentIndex = idx.toString() }

    const updateImage = () => {
      const images = getImages()
      const idx = getCurrentIndex()
      if (images.length > 0 && lightboxImage) {
        lightboxImage.src = images[idx]
        if (lightboxCounter) {
          lightboxCounter.textContent = `${idx + 1} / ${images.length}`
        }
      }
    }

    const nextImage = () => {
      const images = getImages()
      const newIdx = (getCurrentIndex() + 1) % images.length
      setCurrentIndex(newIdx)
      updateImage()
    }

    const prevImage = () => {
      const images = getImages()
      const newIdx = (getCurrentIndex() - 1 + images.length) % images.length
      setCurrentIndex(newIdx)
      updateImage()
    }

    const openLightbox = () => {
      updateImage()
      dialog.showModal()
    }

    const closeLightbox = () => {
      dialog.close()
    }

    // Event listeners - support both legacy pdp: events and new lightbox: events
    dialog.addEventListener('pdp:open-lightbox', openLightbox)
    dialog.addEventListener('pdp:close-lightbox', closeLightbox)
    dialog.addEventListener('pdp:next-image', nextImage)
    dialog.addEventListener('pdp:prev-image', prevImage)
    dialog.addEventListener('lightbox:open', openLightbox)
    dialog.addEventListener('lightbox:close', closeLightbox)
    dialog.addEventListener('lightbox:next', nextImage)
    dialog.addEventListener('lightbox:prev', prevImage)

    // Close on clicking backdrop
    dialog.addEventListener('click', (e) => {
      if (e.target === dialog) {
        closeLightbox()
      }
    })

    // Keyboard navigation
    dialog.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight') {
        nextImage()
      } else if (e.key === 'ArrowLeft') {
        prevImage()
      } else if (e.key === 'Escape') {
        closeLightbox()
      }
    })

    // Store cleanup function
    this.cleanup = () => {
      dialog.removeEventListener('pdp:open-lightbox', openLightbox)
      dialog.removeEventListener('pdp:close-lightbox', closeLightbox)
      dialog.removeEventListener('pdp:next-image', nextImage)
      dialog.removeEventListener('pdp:prev-image', prevImage)
      dialog.removeEventListener('lightbox:open', openLightbox)
      dialog.removeEventListener('lightbox:close', closeLightbox)
      dialog.removeEventListener('lightbox:next', nextImage)
      dialog.removeEventListener('lightbox:prev', prevImage)
    }
  },

  destroyed() {
    if (this.cleanup) {
      this.cleanup()
    }
  }
}

const ProductImageScroll = {
  mounted() {
    this.el.addEventListener('scroll', () => {
      const {dots, thumbButtons, lightbox} = this._queryDom()
      const index = Math.round(this.el.scrollLeft / this.el.offsetWidth)
      dots.forEach((dot, i) => {
        dot.classList.toggle('product-image-dot-active', i === index)
      })
      thumbButtons.forEach((btn, i) => {
        btn.classList.toggle('pdp-thumbnail-active', i === index)
      })
      if (lightbox) lightbox.dataset.currentIndex = index.toString()
    }, {passive: true})

    this.el.addEventListener('pdp:scroll-to', (e) => {
      const index = e.detail.index
      this.el.scrollTo({left: index * this.el.offsetWidth, behavior: 'smooth'})
    })

    this.el.addEventListener('pdp:scroll-prev', () => {
      const current = Math.round(this.el.scrollLeft / this.el.offsetWidth)
      const count = this.el.children.length
      const target = (current - 1 + count) % count
      this.el.scrollTo({left: target * this.el.offsetWidth, behavior: 'smooth'})
    })

    this.el.addEventListener('pdp:scroll-next', () => {
      const current = Math.round(this.el.scrollLeft / this.el.offsetWidth)
      const count = this.el.children.length
      const target = (current + 1) % count
      this.el.scrollTo({left: target * this.el.offsetWidth, behavior: 'smooth'})
    })
  },

  updated() {
    this.el.scrollTo({left: 0, behavior: 'instant'})
  },

  _queryDom() {
    const container = this.el.parentElement
    const dotsEl = container.querySelector('.product-image-dots')
    return {
      dots: dotsEl ? dotsEl.querySelectorAll('.product-image-dot') : [],
      thumbButtons: container.parentElement.querySelector('.pdp-gallery-thumbs')
        ?.querySelectorAll('.pdp-thumbnail') || [],
      lightbox: container.parentElement.querySelector('dialog')
    }
  }
}

// Hook for search modal keyboard navigation and shortcuts
const SearchModal = {
  mounted() {
    this.selectedIndex = -1

    this._globalKeydown = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault()
        if (this.isOpen()) {
          this.close()
        } else {
          this.open()
        }
      }
    }
    document.addEventListener("keydown", this._globalKeydown)

    // Prevent the search trigger link from navigating when the modal is available
    this._searchTrigger = document.querySelector('a[aria-label="Search"]')
    if (this._searchTrigger) {
      this._preventNav = (e) => e.preventDefault()
      this._searchTrigger.addEventListener("click", this._preventNav)
    }

    this.el.addEventListener("open-search", () => this.open())
    this.el.addEventListener("close-search", () => this.close())

    this.el.addEventListener("keydown", (e) => {
      if (!this.isOpen()) return

      switch (e.key) {
        case "Escape":
          e.preventDefault()
          this.close()
          break
        case "ArrowDown":
          e.preventDefault()
          this.moveSelection(1)
          break
        case "ArrowUp":
          e.preventDefault()
          this.moveSelection(-1)
          break
        case "Enter":
          e.preventDefault()
          this.navigateToSelected()
          break
      }
    })
  },

  destroyed() {
    document.removeEventListener("keydown", this._globalKeydown)
    if (this._searchTrigger && this._preventNav) {
      this._searchTrigger.removeEventListener("click", this._preventNav)
    }
  },

  updated() {
    if (this._closing) this.el.style.display = "none"
    this.selectedIndex = -1
    this.updateHighlight()
  },

  isOpen() {
    return this.el.style.display !== "none" && this.el.style.display !== ""
  },

  open() {
    this._closing = false
    this.el.style.display = "flex"
    this.pushEvent("open_search", {})
    const input = this.el.querySelector("#search-input")
    if (input) {
      input.focus()
      input.select()
    }
    this.selectedIndex = -1
    this.updateHighlight()
  },

  close() {
    this._closing = true
    this.el.style.display = "none"
    this.pushEvent("clear_search", {})
    this.selectedIndex = -1
    this.updateHighlight()
  },

  getResults() {
    return this.el.querySelectorAll('[role="option"]')
  },

  moveSelection(delta) {
    const results = this.getResults()
    if (results.length === 0) return

    this.selectedIndex += delta
    if (this.selectedIndex < -1) this.selectedIndex = results.length - 1
    if (this.selectedIndex >= results.length) this.selectedIndex = 0

    this.updateHighlight()
  },

  updateHighlight() {
    const results = this.getResults()
    results.forEach((r, i) => {
      const isSelected = i === this.selectedIndex
      r.style.background = isSelected ? "var(--t-surface-sunken)" : ""
      r.setAttribute("aria-selected", isSelected)
    })

    if (this.selectedIndex >= 0 && results[this.selectedIndex]) {
      results[this.selectedIndex].scrollIntoView({ block: "nearest" })
    }
  },

  navigateToSelected() {
    const results = this.getResults()
    const index = this.selectedIndex >= 0 ? this.selectedIndex : 0
    if (results[index]) {
      const link = results[index].querySelector("a")
      if (link) {
        this.el.style.display = "none"
        link.click()
      }
    }
  }
}

// Mobile nav drawer - slides in from the left
const MobileNavDrawer = {
  mounted() {
    this.el.addEventListener("open-mobile-nav", () => this.open())
    this.el.addEventListener("close-mobile-nav", () => this.close())

    // Close on Escape key
    this._keydown = (e) => {
      if (e.key === "Escape" && this.isOpen()) {
        e.preventDefault()
        this.close()
      }
    }
    document.addEventListener("keydown", this._keydown)

    // Prevent hamburger button navigation on no-JS fallback
    this._hamburger = document.querySelector('.header-hamburger')
    if (this._hamburger) {
      this._preventNav = (e) => e.preventDefault()
      this._hamburger.addEventListener("click", this._preventNav)
    }
  },

  destroyed() {
    document.removeEventListener("keydown", this._keydown)
    if (this._hamburger && this._preventNav) {
      this._hamburger.removeEventListener("click", this._preventNav)
    }
  },

  isOpen() {
    return this.el.classList.contains("is-open")
  },

  open() {
    this.el.classList.add("is-open")
    document.body.style.overflow = "hidden"
    // Focus the close button for accessibility
    const closeBtn = this.el.querySelector(".mobile-nav-close")
    if (closeBtn) closeBtn.focus()
  },

  close() {
    this.el.classList.remove("is-open")
    document.body.style.overflow = ""
    // Return focus to hamburger button
    if (this._hamburger) this._hamburger.focus()
  }
}

// Flex-wrap base → horizontal scroll enhancement for collection category pills.
// If the pills wrap past 2 rows on mobile, switches to single-row scroll
// and scrolls the active pill into view.
const CollectionFilters = {
  mounted() { this._enhance() },
  updated() { this._enhance() },
  _enhance() {
    const nav = this.el
    const ul = nav.querySelector("ul")
    if (!ul) return

    // Reset to measure natural wrap height
    nav.classList.remove("is-scrollable")
    const firstItem = ul.querySelector("li")
    if (!firstItem) return

    const rowHeight = firstItem.offsetHeight
    const wrapsToMany = ul.scrollHeight > rowHeight * 2.5

    if (wrapsToMany && window.innerWidth < 640) {
      nav.classList.add("is-scrollable")
      const active = nav.querySelector("[aria-current]")
      if (active) {
        active.scrollIntoView({ inline: "center", block: "nearest", behavior: "instant" })
      }
    }
  }
}

// Analytics export: reads the current period and filters from the DOM at click time
// so the download URL is always correct, even if clicked before the LiveView re-render.
const AnalyticsExport = {
  mounted() {
    this.el.addEventListener("click", (e) => {
      e.preventDefault()
      const params = new URLSearchParams()
      params.set("period", this.el.getAttribute("data-period") || "30d")
      document.querySelectorAll("[data-filter-dimension]").forEach((span) => {
        const dim = span.getAttribute("data-filter-dimension")
        const val = span.getAttribute("data-filter-value")
        if (dim && val) params.set(`filter[${dim}]`, val)
      })
      // Use a temporary anchor click so the download doesn't navigate away and
      // kill the LiveView WebSocket.
      const a = document.createElement("a")
      a.href = `/admin/analytics/export?${params}`
      a.download = ""
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
    })
  }
}

// Analytics: send screen width for device classification (Layer 3)
const AnalyticsInit = {
  mounted() {
    this.pushEvent("analytics:screen", { width: window.innerWidth })
  }
}

// Analytics: chart tooltip on hover/tap
const ChartTooltip = {
  mounted() { this._setup() },
  updated() { this._setup() },

  _setup() {
    // Re-query after LiveView patches
    this.tooltip = this.el.querySelector("[data-tooltip]")
    this.bars = this.el.querySelector("[data-bars]")
    if (!this.tooltip || !this.bars) return

    // Clean up previous listeners if re-setting up
    if (this._cleanup) this._cleanup()

    const onMove = (e) => {
      const clientX = e.touches ? e.touches[0].clientX : e.clientX
      const bar = this._barAt(clientX)
      if (bar) this._show(bar, clientX)
    }

    const onLeave = () => this._hide()

    const onDocTap = (e) => {
      if (!this.el.contains(e.target)) this._hide()
    }

    this.bars.addEventListener("mousemove", onMove)
    this.bars.addEventListener("mouseleave", onLeave)
    this.bars.addEventListener("touchstart", onMove, { passive: true })
    document.addEventListener("touchstart", onDocTap, { passive: true })

    this._cleanup = () => {
      this.bars.removeEventListener("mousemove", onMove)
      this.bars.removeEventListener("mouseleave", onLeave)
      this.bars.removeEventListener("touchstart", onMove)
      document.removeEventListener("touchstart", onDocTap)
    }
  },

  destroyed() {
    if (this._cleanup) this._cleanup()
  },

  _barAt(clientX) {
    const children = this.bars.children
    for (let i = 0; i < children.length; i++) {
      const rect = children[i].getBoundingClientRect()
      if (clientX >= rect.left && clientX <= rect.right) return children[i]
    }
    return null
  },

  _show(bar, clientX) {
    const label = bar.dataset.label
    const visitors = bar.dataset.visitors
    if (!label) return

    this.tooltip.textContent = `${label}: ${visitors} visitor${visitors === "1" ? "" : "s"}`
    this.tooltip.style.display = "block"

    // Position: centered on cursor, clamped to chart bounds
    const chartRect = this.el.getBoundingClientRect()
    const tipWidth = this.tooltip.offsetWidth
    let left = clientX - chartRect.left - tipWidth / 2
    left = Math.max(0, Math.min(left, chartRect.width - tipWidth))
    this.tooltip.style.left = left + "px"
  },

  _hide() {
    if (this.tooltip) this.tooltip.style.display = "none"
  }
}

// Warns before navigating away from pages with unsaved changes
const DirtyGuard = {
  mounted() {
    this._beforeUnload = (e) => {
      if (this.el.dataset.dirty === "true") {
        e.preventDefault()
        e.returnValue = ""
      }
    }
    window.addEventListener("beforeunload", this._beforeUnload)
  },

  destroyed() {
    window.removeEventListener("beforeunload", this._beforeUnload)
  }
}

// EditorSheet: handles click-outside, Escape, navigation guard, and mobile drag-to-resize
const EditorSheet = {
  mounted() {
    // Close on Escape key
    this._onKeydown = (e) => {
      if (e.key === "Escape" && this._getState() === "open") {
        e.preventDefault()
        this._close()
      }
    }
    document.addEventListener("keydown", this._onKeydown)

    // Navigation guard: warn on browser close/refresh
    this._beforeUnload = (e) => {
      if (this.el.dataset.dirty === "true") {
        e.preventDefault()
        e.returnValue = ""
      }
    }
    window.addEventListener("beforeunload", this._beforeUnload)

    // Navigation guard: intercept LiveView link clicks
    this._clickGuard = (e) => {
      if (this.el.dataset.dirty !== "true") return

      const link = e.target.closest("a[data-phx-link]")
      if (!link) return

      // Don't block clicks inside the editor itself
      if (this.el.contains(link)) return

      // Block navigation and show custom modal
      e.preventDefault()
      e.stopImmediatePropagation()
      this.pushEvent("editor_nav_blocked", { href: link.getAttribute("href") })
    }
    document.addEventListener("click", this._clickGuard, true)

    // Handle navigation after save/discard
    this.handleEvent("editor_navigate", ({ href }) => {
      window.location.href = href
    })

    // Restore saved height on mobile and mark as already opened
    this._restoreSavedHeight()
    this._hasDragged = !!localStorage.getItem("editor-panel-height")
    if (this._hasDragged) {
      this.el.classList.add("has-dragged")
    }

    // Set up drag handle for mobile resizing
    this._setupDragHandle()
  },

  updated() {
    // Restore state after LiveView re-renders the element
    this._restoreSavedHeight()
    if (this._hasDragged) {
      this.el.classList.add("has-dragged")
    }
  },

  destroyed() {
    document.removeEventListener("keydown", this._onKeydown)
    window.removeEventListener("beforeunload", this._beforeUnload)
    document.removeEventListener("click", this._clickGuard, true)
    this._cleanupDragHandle()
  },

  _getState() {
    return this.el.dataset.state || "collapsed"
  },

  _close() {
    const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
    if (prefersReducedMotion) {
      this.el.setAttribute("aria-hidden", "true")
      this.pushEvent("editor_set_sheet_state", { state: "collapsed" })
      this._announce("Editor collapsed")
      return
    }

    this.el.classList.add("closing")
    this.el.addEventListener("animationend", () => {
      this.el.classList.remove("closing")
      this.el.setAttribute("aria-hidden", "true")
      this.pushEvent("editor_set_sheet_state", { state: "collapsed" })
      this._announce("Editor collapsed")
    }, { once: true })
  },

  _announce(message) {
    const region = document.getElementById("editor-live-region")
    if (region) {
      region.textContent = message
    }
  },

  // ── Mobile drag-to-resize ──
  _isMobile() {
    return window.matchMedia("(max-width: 767px)").matches
  },

  _restoreSavedHeight() {
    if (!this._isMobile()) return
    const savedHeight = localStorage.getItem("editor-panel-height")
    if (savedHeight) {
      this.el.style.setProperty("--editor-panel-height", savedHeight)
    }
  },

  _setupDragHandle() {
    const handle = this.el.querySelector("[data-drag-handle]")
    if (!handle) return

    this._dragHandle = handle
    this._isDragging = false
    this._startY = 0
    this._startHeight = 0

    // Touch events
    this._onTouchStart = (e) => this._handleDragStart(e.touches[0].clientY)
    this._onTouchMove = (e) => {
      if (!this._isDragging) return
      e.preventDefault()
      this._handleDragMove(e.touches[0].clientY)
    }
    this._onTouchEnd = () => this._handleDragEnd()

    // Mouse events (for testing on desktop)
    this._onMouseDown = (e) => {
      e.preventDefault()
      this._handleDragStart(e.clientY)
      document.addEventListener("mousemove", this._onMouseMove)
      document.addEventListener("mouseup", this._onMouseUp)
    }
    this._onMouseMove = (e) => {
      if (!this._isDragging) return
      this._handleDragMove(e.clientY)
    }
    this._onMouseUp = () => {
      this._handleDragEnd()
      document.removeEventListener("mousemove", this._onMouseMove)
      document.removeEventListener("mouseup", this._onMouseUp)
    }

    handle.addEventListener("touchstart", this._onTouchStart, { passive: true })
    document.addEventListener("touchmove", this._onTouchMove, { passive: false })
    document.addEventListener("touchend", this._onTouchEnd)
    handle.addEventListener("mousedown", this._onMouseDown)
  },

  _cleanupDragHandle() {
    if (!this._dragHandle) return
    this._dragHandle.removeEventListener("touchstart", this._onTouchStart)
    document.removeEventListener("touchmove", this._onTouchMove)
    document.removeEventListener("touchend", this._onTouchEnd)
    this._dragHandle.removeEventListener("mousedown", this._onMouseDown)
    document.removeEventListener("mousemove", this._onMouseMove)
    document.removeEventListener("mouseup", this._onMouseUp)
  },

  _handleDragStart(clientY) {
    if (!this._isMobile()) return
    this._isDragging = true
    this._startY = clientY
    this._startHeight = this.el.offsetHeight
    this.el.classList.add("dragging")
  },

  _handleDragMove(clientY) {
    if (!this._isDragging) return
    const deltaY = this._startY - clientY
    const newHeight = this._startHeight + deltaY
    const vh = window.innerHeight
    const minHeight = 200
    const maxHeight = vh * 0.9
    const clampedHeight = Math.max(minHeight, Math.min(newHeight, maxHeight))
    this.el.style.setProperty("--editor-panel-height", `${clampedHeight}px`)
  },

  _handleDragEnd() {
    if (!this._isDragging) return
    this._isDragging = false
    // Save the height preference
    const currentHeight = getComputedStyle(this.el).getPropertyValue("--editor-panel-height")
    if (currentHeight) {
      localStorage.setItem("editor-panel-height", currentHeight.trim())
    }
    // Mark that we've dragged so the open animation doesn't replay
    this._hasDragged = true
    this.el.classList.add("has-dragged")
    this.el.classList.remove("dragging")
  }
}

// Clipboard: copy text from a target element to clipboard
const Clipboard = {
  mounted() {
    this.el.addEventListener("click", () => {
      const targetId = this.el.dataset.copyTarget
      const target = document.getElementById(targetId)
      if (!target) return

      const text = target.textContent.trim()
      this._copyText(text).then(() => {
        const span = this.el.querySelector("span")
        if (span) {
          const original = span.textContent
          span.textContent = "Copied!"
          setTimeout(() => { span.textContent = original }, 1500)
        }
      })
    })
  },

  _copyText(text) {
    // Modern API (requires HTTPS or localhost)
    if (navigator.clipboard && navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text)
    }
    // Fallback for HTTP contexts
    return new Promise((resolve) => {
      const textarea = document.createElement("textarea")
      textarea.value = text
      textarea.style.position = "fixed"
      textarea.style.opacity = "0"
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand("copy")
      document.body.removeChild(textarea)
      resolve()
    })
  }
}

// Preserve <details> open state across LiveView re-renders
// Morphdom removes the open attribute when server doesn't send it,
// so we store the state locally and restore it after each update.
const DetailsPreserver = {
  mounted() {
    this._saveState()
  },
  beforeUpdate() {
    this._saveState()
  },
  updated() {
    this._restoreState()
  },
  _saveState() {
    this._wasOpen = this.el.open
  },
  _restoreState() {
    if (this._wasOpen !== undefined) {
      this.el.open = this._wasOpen
    }
  }
}

// Ctrl+Z / Ctrl+Shift+Z undo/redo for page editors
// Note: Navigation guards are now handled by EditorSheet hook
const EditorKeyboard = {
  mounted() {
    const prefix = this.el.dataset.eventPrefix || ""
    this._keydown = (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === "z") {
        e.preventDefault()
        if (e.shiftKey) {
          this.pushEvent(prefix + "redo")
        } else {
          this.pushEvent(prefix + "undo")
        }
      }
    }
    document.addEventListener("keydown", this._keydown)
  },

  destroyed() {
    document.removeEventListener("keydown", this._keydown)
  }
}

// Hook to trigger file downloads from LiveView
const Download = {
  mounted() {
    this.handleEvent("download", ({filename, content, content_type}) => {
      const blob = new Blob([Uint8Array.from(atob(content), c => c.charCodeAt(0))], {type: content_type})
      const url = URL.createObjectURL(blob)
      const a = document.createElement("a")
      a.href = url
      a.download = filename
      a.click()
      URL.revokeObjectURL(url)
    })
  }
}

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken, screen_width: window.innerWidth},
  hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, MobileNavDrawer, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, Clipboard, DetailsPreserver, DirtyGuard, EditorKeyboard, EditorSheet, Download},
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// Scroll preview frame to top when changing pages
window.addEventListener("phx:scroll-preview-top", (e) => {
  const previewFrame = document.querySelector('.preview-frame')
  if (previewFrame) {
    previewFrame.scrollTop = 0
  }
})

// Scroll to top on page navigation (patch navigation within LiveView)
window.addEventListener("phx:scroll-top", () => {
  window.scrollTo({top: 0, behavior: 'instant'})
})

// Scroll element into view (used by flash messages)
window.addEventListener("scroll-into-view", (e) => {
  e.target.scrollIntoView({behavior: 'smooth', block: 'nearest'})
})

// connect if there are any LiveViews on the page
liveSocket.connect()

// Suppress connection error banners during intentional navigation (refresh, link click).
// beforeunload fires on navigation but NOT on genuine connection drops.
window.addEventListener("beforeunload", () => {
  document.body.classList.add("navigating-away")
})

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
    // Enable server log streaming to client.
    // Disable with reloader.disableServerLogs()
    reloader.enableServerLogs()

    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
    //
    //   * click with "c" key pressed to open at caller location
    //   * click with "d" key pressed to open at function component definition location
    let keyDown
    window.addEventListener("keydown", e => keyDown = e.key)
    window.addEventListener("keyup", e => keyDown = null)
    window.addEventListener("click", e => {
      if(keyDown === "c"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtCaller(e.target)
      } else if(keyDown === "d"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtDef(e.target)
      }
    }, true)

    window.liveReloader = reloader
  })
}

