// Touch Therapies CMS — admin behaviours: rich text editor, image uploads,
// conditional and self-filling fields, and the structured editor that replaces
// raw-JSON textareas.

const BASE = window.__BASE || ''

const csrfOf = (form) =>
  form?.querySelector('input[name=_csrf]')?.value || window.__CSRF || ''

// `name` matters: the upload endpoint validates the file extension, and a Blob
// handed straight to FormData arrives named "blob" with no extension at all.
async function uploadImage(file, csrf, name) {
  const body = new FormData()
  body.append('file', file, name || file.name || 'upload.png')
  body.append('_csrf', csrf)
  const res = await fetch(BASE + '/admin/media/upload', { method: 'POST', body })
  const json = await res.json()
  if (!json.data?.url) throw new Error(json.error || 'Upload failed')
  return json.data.url
}

// Rich text: TinyMCE over the `richtext` textareas. Self-hosted under
// /assets/tinymce (GPL build — hence license_key), so there is no API key and
// no cdn.tiny.cloud dependency at runtime. TinyMCE syncs the editor back into
// its textarea on form submit by itself.
if (window.tinymce && document.querySelector('textarea.rte')) {
  window.tinymce.init({
    selector: 'textarea.rte',
    // Resolves skins/plugins correctly when the CMS is installed in a subfolder.
    base_url: BASE + '/assets/tinymce',
    license_key: 'gpl',
    plugins: 'advlist autolink autoresize code image link lists',
    toolbar: 'blocks | bold italic | bullist numlist | link image | removeformat code',
    menubar: false,
    statusbar: false,
    promotion: false,
    branding: false,
    // The front end styles h2/h3/p/ul/a/img inside the page body; offering more
    // block types than it can render would just produce unstyled output.
    block_formats: 'Text=p; Heading=h2; Sub-heading=h3',
    content_css: BASE + '/assets/editor-content.css',
    // Keep /uploads/... paths exactly as stored — the front end reads them raw.
    convert_urls: false,
    autoresize_bottom_margin: 20,
    min_height: 460,
    // Drag-drop and paste straight into the editor.
    images_upload_handler: (blobInfo) =>
      uploadImage(blobInfo.blob(), csrfOf(document.querySelector('form.form')), blobInfo.filename()),
    // ...and the "browse" button in the Image dialog.
    file_picker_types: 'image',
    file_picker_callback: pickAndUploadImage,
  })
}

/** Backs the Image dialog's browse button: pick a file, upload, hand back the URL. */
function pickAndUploadImage(callback) {
  const picker = document.createElement('input')
  picker.type = 'file'
  picker.accept = 'image/*'
  // The input must be in the DOM for the file dialog's "change" event to fire
  // reliably (Safari and some others never fire it for a detached input, so the
  // chosen image was silently dropped). Kept hidden and removed after use.
  picker.style.display = 'none'
  document.body.appendChild(picker)
  picker.addEventListener('change', async () => {
    const file = picker.files?.[0]
    if (!file) {
      picker.remove()
      return
    }
    try {
      const url = await uploadImage(file, csrfOf(document.querySelector('form.form')), file.name)
      callback(url, { alt: '' })
    } catch (e) {
      alert(e.message || 'Upload failed')
    } finally {
      picker.remove()
    }
  })
  picker.click()
}

// Image upload widgets on `image` fields: POST the file, drop the URL in the input.
document.querySelectorAll('.upload-btn input[type=file]').forEach((input) => {
  input.addEventListener('change', async () => {
    const file = input.files?.[0]
    if (!file) return
    const target = document.getElementById(input.dataset.target)
    const label = input.closest('.upload-btn')
    label.style.opacity = '0.5'
    try {
      const url = await uploadImage(file, csrfOf(input.closest('form')))
      target.value = url
      // Setting .value in script fires no event, and `data-show-when` fields
      // listen for one to know the image now exists.
      target.dispatchEvent(new Event('input', { bubbles: true }))
    } catch (e) {
      alert(e.message || 'Upload failed')
    } finally {
      label.style.opacity = ''
      input.value = ''
    }
  })
})

// `image` fields: keep the thumbnail in step with the URL, however it got there.
document.querySelectorAll('.image-field input[type=text]').forEach((input) => {
  const preview = input.closest('.field')?.querySelector('.preview')
  if (!preview) return
  const sync = () => {
    preview.hidden = !input.value
    if (input.value) preview.src = input.value
  }
  input.addEventListener('input', sync)
  sync()
})

// `showWhen` fields: only shown once the field they depend on has a value.
// A row whose fields have all gone hides with them.
document.querySelectorAll('[data-show-when]').forEach((field) => {
  const source = document.getElementById(field.dataset.showWhen)
  if (!source) return
  const sync = () => {
    field.hidden = !source.value.trim()
    const row = field.closest('.field-row')
    if (row) row.hidden = ![...row.children].some((f) => !f.hidden)
  }
  source.addEventListener('input', sync)
  source.addEventListener('change', sync)
  sync()
})

// A required field inside the collapsed Advanced block (the slug) can fail
// validation somewhere the browser cannot focus, so Save appears to do nothing
// and the field to fix is out of sight. Open the block as it fails, before the
// browser tries to report. 'invalid' does not bubble — hence capture.
document.querySelectorAll('form').forEach((form) => {
  form.addEventListener(
    'invalid',
    (e) => {
      const details = e.target.closest('details')
      if (details) details.open = true
    },
    true,
  )
})

const slugify = (s) =>
  s
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '') // strip the accents NFD just split off
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')

// `autoFrom` fields (the slug) fill themselves in from another field — but only
// while they still agree with it. An existing page's slug is already part of its
// public URL and of any link pointing at it, so once it has drifted from the
// title, retitling the page must not silently move it.
document.querySelectorAll('input[data-auto-from]').forEach((target) => {
  const source = document.getElementById(target.dataset.autoFrom)
  if (!source) return
  let linked = !target.value || target.value === slugify(source.value)
  target.addEventListener('input', () => {
    linked = false
  })
  source.addEventListener('input', () => {
    if (linked) target.value = slugify(source.value)
  })
})

// ---- Structured JSON editor -------------------------------------------------
// `json` fields (and JSON settings) render as a raw textarea with class .mono.
// When the content parses, we hide the textarea and build labeled inputs from
// the data: nested objects become groups, arrays become editable item lists,
// image-like keys get an upload button. The textarea stays the submitted
// source of truth — it is re-serialised on save, and "Edit as raw JSON"
// toggles back at any time.

const IMAGE_KEY = /(^|_)(image|img|photo|src|background|logo)(_|$)/i

const labelFor = (key) =>
  key.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())

function el(tag, className, text) {
  const node = document.createElement(tag)
  if (className) node.className = className
  if (text !== undefined) node.textContent = text
  return node
}

function buildEditor(value, key, form, shape) {
  if (Array.isArray(value)) return buildArray(value, key, form, shape)
  if (value && typeof value === 'object') return buildObject(value, key, form)
  return buildScalar(value, key, form)
}

function buildObject(value, key, form) {
  const wrap = el('div', key ? 'je-group' : 'je')
  if (key) wrap.appendChild(el('div', 'je-legend', labelFor(key)))
  const fields = Object.entries(value).map(([k, v]) => {
    const child = buildEditor(v, k, form)
    wrap.appendChild(child.node)
    return [k, child]
  })
  return {
    node: wrap,
    read: () => Object.fromEntries(fields.map(([k, c]) => [k, c.read()])),
  }
}

function buildArray(value, key, form, shape) {
  const wrap = el('div', key ? 'je-group' : 'je')
  if (key) wrap.appendChild(el('div', 'je-legend', labelFor(key)))

  // Image gallery: list of image URLs, each with its own upload button/preview.
  if (shape === 'imageList' && value.every((v) => typeof v === 'string')) {
    return buildImageList(value, wrap, form)
  }

  // List of plain strings (paragraphs, package inclusions, tags): one per line.
  if (value.every((v) => typeof v === 'string')) {
    const input = document.createElement('textarea')
    input.rows = Math.min(10, Math.max(3, value.length + 1))
    input.value = value.join('\n')
    wrap.append(input, el('small', '', 'One entry per line.'))
    return {
      node: wrap,
      read: () => input.value.split('\n').map((s) => s.trim()).filter(Boolean),
    }
  }

  // List of uniform objects (e.g. feature cards): item groups with add/remove.
  if (value.length && value.every((v) => v && typeof v === 'object' && !Array.isArray(v))) {
    const template = Object.keys(value[0])
    const list = el('div', 'je-list')
    wrap.appendChild(list)
    let items = []
    const addItem = (obj) => {
      const child = buildObject(obj, null, form)
      const item = el('div', 'je-item')
      const remove = el('button', 'btn sm danger je-remove', 'Remove')
      remove.type = 'button'
      const entry = { child }
      remove.addEventListener('click', () => {
        items = items.filter((i) => i !== entry)
        item.remove()
      })
      item.append(child.node, remove)
      list.appendChild(item)
      items.push(entry)
    }
    value.forEach(addItem)
    const add = el('button', 'btn sm je-add', '+ Add item')
    add.type = 'button'
    add.addEventListener('click', () =>
      addItem(Object.fromEntries(template.map((k) => [k, '']))))
    wrap.appendChild(add)
    return { node: wrap, read: () => items.map((i) => i.child.read()) }
  }

  // Anything irregular stays raw JSON for just this part.
  const input = document.createElement('textarea')
  input.className = 'mono'
  input.rows = 5
  input.value = JSON.stringify(value, null, 2)
  wrap.appendChild(input)
  return { node: wrap, read: () => JSON.parse(input.value) }
}

function buildScalar(value, key, form) {
  const wrap = el('div', 'je-field')
  if (key) wrap.appendChild(el('label', '', labelFor(key)))

  if (typeof value === 'boolean') {
    const label = el('label', 'switch')
    const box = document.createElement('input')
    box.type = 'checkbox'
    box.checked = value
    label.append(box, el('span', '', 'Enabled'))
    wrap.appendChild(label)
    return { node: wrap, read: () => box.checked }
  }

  if (typeof value === 'number') {
    const input = document.createElement('input')
    input.type = 'number'
    input.step = 'any'
    input.value = String(value)
    wrap.appendChild(input)
    return {
      node: wrap,
      read: () => (input.value.trim() === '' ? null : Number(input.value)),
    }
  }

  const str = value === null ? '' : String(value)
  if (key && IMAGE_KEY.test(key)) return buildImageField(str, wrap, form)

  let input
  if (str.length > 90 || str.includes('\n')) {
    input = document.createElement('textarea')
    input.rows = 3
  } else {
    input = document.createElement('input')
    input.type = 'text'
  }
  input.value = str
  wrap.appendChild(input)
  return { node: wrap, read: () => input.value }
}

function buildImageField(value, wrap, form) {
  const row = el('div', 'image-field')
  const input = document.createElement('input')
  input.type = 'text'
  input.value = value
  input.placeholder = 'https://… or upload →'
  const label = el('label', 'btn sm upload-btn', 'Upload')
  const file = document.createElement('input')
  file.type = 'file'
  file.accept = 'image/*'
  file.hidden = true
  label.appendChild(file)
  row.append(input, label)

  const preview = el('img', 'preview')
  preview.alt = ''
  const syncPreview = () => {
    if (input.value) preview.src = input.value
    preview.style.display = input.value ? '' : 'none'
  }
  input.addEventListener('input', syncPreview)
  syncPreview()

  file.addEventListener('change', async () => {
    const f = file.files?.[0]
    if (!f) return
    label.style.opacity = '0.5'
    try {
      input.value = await uploadImage(f, csrfOf(form))
      syncPreview()
    } catch (e) {
      alert(e.message || 'Upload failed')
    } finally {
      label.style.opacity = ''
      file.value = ''
    }
  })

  wrap.append(row, preview)
  return { node: wrap, read: () => input.value }
}

// A gallery: one upload-with-preview row per photo, with add/remove — the
// image-field equivalent of the plain-object item list above.
function buildImageList(value, wrap, form) {
  const list = el('div', 'je-list')
  wrap.appendChild(list)
  let items = []
  const addItem = (url) => {
    const fieldWrap = el('div', 'je-field')
    const child = buildImageField(url, fieldWrap, form)
    const item = el('div', 'je-item')
    const remove = el('button', 'btn sm danger je-remove', 'Remove')
    remove.type = 'button'
    const entry = { child }
    remove.addEventListener('click', () => {
      items = items.filter((i) => i !== entry)
      item.remove()
    })
    item.append(child.node, remove)
    list.appendChild(item)
    items.push(entry)
  }
  value.forEach(addItem)
  const add = el('button', 'btn sm je-add', '+ Add photo')
  add.type = 'button'
  add.addEventListener('click', () => addItem(''))
  wrap.appendChild(add)
  return { node: wrap, read: () => items.map((i) => i.child.read()).filter(Boolean) }
}

document.querySelectorAll('form textarea.mono').forEach((source) => {
  const form = source.closest('form')
  const help = source.closest('.field')?.querySelector('small')
  const toggle = el('button', 'je-toggle', 'Edit as raw JSON')
  toggle.type = 'button'
  let root = null

  const openEditor = () => {
    let parsed
    if (source.value.trim() === '' && ['list', 'imageList'].includes(source.dataset.shape)) {
      parsed = [] // empty list field: open the easy editor instead of raw JSON
    } else {
      try {
        parsed = JSON.parse(source.value)
      } catch {
        return false
      }
    }
    if (parsed === null || typeof parsed !== 'object') return false
    root = buildEditor(parsed, null, form, source.dataset.shape)
    root.node.classList.add('je')
    toggle.before(root.node)
    toggle.textContent = 'Edit as raw JSON'
    source.hidden = true
    if (help) help.hidden = true
    return true
  }

  const closeEditor = () => {
    source.value = JSON.stringify(root.read())
    root.node.remove()
    root = null
    source.hidden = false
    if (help) help.hidden = false
    toggle.textContent = 'Back to the easy editor'
  }

  toggle.addEventListener('click', () => {
    if (root) {
      try {
        closeEditor()
      } catch {
        alert('A raw JSON sub-section has a syntax error — fix it first.')
      }
    } else if (!openEditor()) {
      alert('Fix the JSON syntax first, then switch back to the easy editor.')
    }
  })

  form?.addEventListener('submit', (e) => {
    if (!root) return
    try {
      source.value = JSON.stringify(root.read())
    } catch {
      e.preventDefault()
      alert('A raw JSON sub-section has a syntax error — fix it before saving.')
    }
  })

  source.after(toggle)
  if (!openEditor()) toggle.remove()
})
